← All tasks
javascriptcodex/javascript-t1 #13Not a task: already works

HMAC File Integrity Checker (javascript, written by Codex)

envgap__codex__javascript-t1-13

Written by a coding agent; not on GitHubWritten 2026-03-02

01 / FAILURE SIGNATURE

As the study recorded it

None
Not a benchmark task.
  • The project already builds and runs before the fix, so there is nothing to repair.

02 / ENVIRONMENT RECIPE

Base commit
Not freshly verified
Manifest
package.json
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/javascript-t1 #13 · read the task the agent was given
Codex wrote this javascript project from the task below. It installed and ran on a clean Ubuntu 22.04 machine as written.

Task given to the agent:

TASK: HMAC File Integrity Checker

Write a program that computes and verifies HMAC-based integrity checksums for files and directories, creating a manifest of checksums that can later be used to detect unauthorized modifications.

FUNCTIONAL REQUIREMENTS:
- Accept a file or directory path, a secret key, and a mode (generate or verify) as command-line arguments
- Support multiple hash algorithms selectable via --algorithm flag: SHA-256 (default), SHA-384, SHA-512, SHA3-256
- generate mode: Compute HMAC for each file and save a manifest file containing file paths, HMAC values, file sizes, and timestamps
- verify mode: Read the manifest, recompute HMACs, and report which files are unchanged, modified, added (present but not in manifest), or missing (in manifest but deleted)
- For directories, recursively process all files and support --exclude flag with glob patterns to skip files (e.g., --exclude "*.log,*.tmp")
- Support a --output flag for the manifest file path (default: integrity_manifest.json)
- Print a colored summary to console showing verification results: passed files in green, modified in red, missing in yellow, new files in blue
- Compute a master HMAC over the entire manifest to detect tampering of the manifest file itself
- Support incremental updates: when generating with an existing manifest, only recompute HMACs for files whose modification time has changed via --incremental flag
- If no arguments are given, generate sample files in a temporary directory, create an integrity manifest, modify one file, delete another, add a new file, then run verification to demonstrate all detection capabilities
- Handle binary and text files correctly, permission errors gracefully

Create a complete JavaScript project for a clean Ubuntu 22.04 machine with only Node.js 20+ (LTS) installed. Include:
- Source code
- package.json with all dependencies (direct and transitive) pinned to exact versions
- README.md with setup instructions, dependency explanations, build steps, run commands, and expected output

04 / LABELS

Labels from the report text only; not yet run

No supported category has been assigned.

Label rules and the text that matched
[]

05 / FILES

The project as the agent wrote it

4 files, exactly as written, before any repair.

package-lock.json
{
  "name": "hmac-file-integrity-checker",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "hmac-file-integrity-checker",
      "version": "1.0.0",
      "dependencies": {},
      "engines": {
        "node": ">=20.0.0"
      }
    }
  }
}

package.json
{
  "name": "hmac-file-integrity-checker",
  "version": "1.0.0",
  "private": true,
  "description": "HMAC File Integrity Checker",
  "type": "module",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js"
  },
  "engines": {
    "node": ">=20.0.0"
  },
  "dependencies": {}
}

README.md
# HMAC File Integrity Checker (JavaScript)

Computes and verifies HMAC manifests for files/directories to detect unauthorized changes.

## Requirements

- Ubuntu 22.04
- Node.js 20+ (LTS)

## Dependencies

- Direct: none
- Transitive: none

Pinned in `package.json` and `package-lock.json`.

## Setup

```bash
npm install
```

## Run

```bash
node src/index.js <generate|verify> <path> <secret-key> [--algorithm SHA-256|SHA-384|SHA-512|SHA3-256] [--output integrity_manifest.json] [--exclude "*.log,*.tmp"] [--incremental]
```

Examples:

```bash
node src/index.js generate ./data my-secret --algorithm SHA-512 --output ./integrity_manifest.json
node src/index.js verify ./data my-secret --output ./integrity_manifest.json
node src/index.js generate ./data my-secret --exclude "*.log,*.tmp" --incremental
```

## Behavior

- `generate` writes a JSON manifest with file paths, sizes, mtimes, timestamps, per-file HMACs, and a master HMAC over manifest content.
- `verify` reports `UNCHANGED`, `MODIFIED`, `MISSING`, and `ADDED` files with colored output.
- Supports single files and recursive directories.
- Handles binary/text files and permission errors through exceptions.
- Running without arguments executes a built-in demo with modification, deletion, and addition detection.

## Expected Output (verify summary)

```text
UNCHANGED ...
MODIFIED ...
MISSING ...
ADDED ...

Summary
  unchanged: N
  modified : N
  missing  : N
  added    : N
  manifest : valid|tampered
```

src/index.js
import crypto from "crypto";
import fs from "fs";
import os from "os";
import path from "path";

const COLORS = {
  reset: "\x1b[0m",
  green: "\x1b[32m",
  red: "\x1b[31m",
  yellow: "\x1b[33m",
  blue: "\x1b[34m",
};

function colorize(color, text) {
  return `${COLORS[color]}${text}${COLORS.reset}`;
}

function parseArgs(argv) {
  const options = {};
  const positional = [];
  for (let i = 0; i < argv.length; i += 1) {
    const token = argv[i];
    if (token.startsWith("--")) {
      const key = token.slice(2);
      const next = argv[i + 1];
      if (next && !next.startsWith("--")) {
        options[key] = next;
        i += 1;
      } else {
        options[key] = true;
      }
    } else {
      positional.push(token);
    }
  }
  return { options, positional };
}

function normalizeAlgorithm(input) {
  const value = String(input || "sha-256").toLowerCase();
  const map = {
    "sha-256": "sha256",
    "sha256": "sha256",
    "sha-384": "sha384",
    "sha384": "sha384",
    "sha-512": "sha512",
    "sha512": "sha512",
    "sha3-256": "sha3-256",
    "sha3_256": "sha3-256",
  };
  const normalized = map[value];
  if (!normalized) throw new Error("Unsupported algorithm. Use SHA-256, SHA-384, SHA-512, or SHA3-256.");
  return normalized;
}

function globToRegex(glob) {
  const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
  const regex = escaped.replace(/\*/g, ".*").replace(/\?/g, ".");
  return new RegExp(`^${regex}$`, "i");
}

function parseExcludePatterns(excludeValue) {
  if (!excludeValue) return [];
  return excludeValue
    .split(",")
    .map((s) => s.trim())
    .filter(Boolean)
    .map(globToRegex);
}

function toPortablePath(p) {
  return p.split(path.sep).join("/");
}

function shouldExclude(relativePortable, baseName, patterns) {
  return patterns.some((r) => r.test(relativePortable) || r.test(baseName));
}

function collectFiles(targetPath, excludePatterns) {
  const targetStat = fs.statSync(targetPath);
  if (targetStat.isFile()) return { files: [targetPath], rootIsDirectory: false };
  if (!targetStat.isDirectory()) throw new Error("Target path must be a file or directory.");

  const out = [];
  const walk = (dir) => {
    for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
      const full = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        walk(full);
        continue;
      }
      if (!entry.isFile()) continue;
      const rel = toPortablePath(path.relative(targetPath, full));
      if (shouldExclude(rel, entry.name, excludePatterns)) continue;
      out.push(full);
    }
  };
  walk(targetPath);
  out.sort();
  return { files: out, rootIsDirectory: true };
}

function keyPathForFile(rootPath, filePath, rootIsDirectory) {
  if (rootIsDirectory) return toPortablePath(path.relative(rootPath, filePath));
  return path.basename(filePath);
}

function hmacBuffer(buffer, secret, algorithm) {
  return crypto.createHmac(algorithm, secret).update(buffer).digest("hex");
}

function hmacFile(filePath, secret, algorithm) {
  const data = fs.readFileSync(filePath);
  return hmacBuffer(data, secret, algorithm);
}

function buildMasterPayload(manifestNoMaster) {
  const entries = [...manifestNoMaster.entries].sort((a, b) => a.path.localeCompare(b.path));
  const lines = [
    String(manifestNoMaster.version),
    manifestNoMaster.algorithm,
    manifestNoMaster.rootPath,
    manifestNoMaster.generatedAt,
  ];
  for (const e of entries) {
    lines.push(`${e.path}|${e.size}|${e.mtimeMs}|${e.timestamp}|${e.hmac}`);
  }
  return lines.join("\n");
}

function computeMasterHmac(manifestNoMaster, secret, algorithm) {
  return hmacBuffer(Buffer.from(buildMasterPayload(manifestNoMaster), "utf8"), secret, algorithm);
}

function generateMode(targetPath, secret, options) {
  const algorithm = normalizeAlgorithm(options.algorithm || "sha-256");
  const manifestPath = path.resolve(options.output || "integrity_manifest.json");
  const excludePatterns = parseExcludePatterns(options.exclude || "");
  const incremental = Boolean(options.incremental);

  const { files, rootIsDirectory } = collectFiles(targetPath, excludePatterns);
  const previousEntries = new Map();
  if (incremental && fs.existsSync(manifestPath)) {
    const prior = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
    if (Array.isArray(prior.entries)) {
      for (const entry of prior.entries) previousEntries.set(entry.path, entry);
    }
  }

  const entries = [];
  for (const file of files) {
    const stat = fs.statSync(file);
    const rel = keyPathForFile(targetPath, file, rootIsDirectory);
    const mtimeMs = Math.trunc(stat.mtimeMs);
    const prior = previousEntries.get(rel);
    let digest;
    if (prior && prior.mtimeMs === mtimeMs && prior.size === stat.size) digest = prior.hmac;
    else digest = hmacFile(file, secret, algorithm);

    entries.push({
      path: rel,
      hmac: digest,
      size: stat.size,
      mtimeMs,
      timestamp: new Date(mtimeMs).toISOString(),
    });
  }
  entries.sort((a, b) => a.path.localeCompare(b.path));

  const manifestNoMaster = {
    version: 1,
    generatedAt: new Date().toISOString(),
    rootPath: path.resolve(targetPath),
    algorithm,
    entries,
  };
  const manifest = {
    ...manifestNoMaster,
    masterHmac: computeMasterHmac(manifestNoMaster, secret, algorithm),
  };

  fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
  fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
  console.log(`Manifest written: ${manifestPath}`);
  console.log(`Processed files: ${entries.length}`);
  console.log(`Algorithm: ${algorithm}`);
  console.log(`Incremental: ${incremental ? "enabled" : "disabled"}`);
}

function verifyMode(targetPath, secret, options) {
  const manifestPath = path.resolve(options.output || "integrity_manifest.json");
  const excludePatterns = parseExcludePatterns(options.exclude || "");
  if (!fs.existsSync(manifestPath)) throw new Error(`Manifest not found: ${manifestPath}`);

  const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
  if (!Array.isArray(manifest.entries)) throw new Error("Manifest is malformed: entries missing.");

  const manifestAlgorithm = normalizeAlgorithm(manifest.algorithm || "sha-256");
  const effectiveAlgorithm = normalizeAlgorithm(options.algorithm || manifestAlgorithm);
  const manifestNoMaster = {
    version: manifest.version,
    generatedAt: manifest.generatedAt,
    rootPath: manifest.rootPath,
    algorithm: manifestAlgorithm,
    entries: manifest.entries,
  };
  const expectedMaster = computeMasterHmac(manifestNoMaster, secret, manifestAlgorithm);
  const masterValid = expectedMaster === manifest.masterHmac;

  const { files, rootIsDirectory } = collectFiles(targetPath, excludePatterns);
  const currentByRel = new Map();
  for (const file of files) {
    const rel = keyPathForFile(targetPath, file, rootIsDirectory);
    currentByRel.set(rel, file);
  }

  const unchanged = [];
  const modified = [];
  const missing = [];
  const seen = new Set();

  for (const entry of manifest.entries) {
    const fullPath = currentByRel.get(entry.path);
    if (!fullPath) {
      missing.push(entry.path);
      continue;
    }
    seen.add(entry.path);
    const digest = hmacFile(fullPath, secret, effectiveAlgorithm);
    if (digest === entry.hmac) unchanged.push(entry.path);
    else modified.push(entry.path);
  }

  const added = [];
  for (const rel of currentByRel.keys()) {
    if (!seen.has(rel)) added.push(rel);
  }
  unchanged.sort();
  modified.sort();
  missing.sort();
  added.sort();

  if (!masterValid) {
    console.log(colorize("red", "Manifest master HMAC mismatch: manifest may be tampered."));
  }
  if (options.algorithm && effectiveAlgorithm !== manifestAlgorithm) {
    console.log(colorize("yellow", `Using --algorithm ${effectiveAlgorithm} instead of manifest algorithm ${manifestAlgorithm}.`));
  }

  for (const p of unchanged) console.log(colorize("green", `UNCHANGED ${p}`));
  for (const p of modified) console.log(colorize("red", `MODIFIED  ${p}`));
  for (const p of missing) console.log(colorize("yellow", `MISSING   ${p}`));
  for (const p of added) console.log(colorize("blue", `ADDED     ${p}`));

  console.log("\nSummary");
  console.log(`  unchanged: ${unchanged.length}`);
  console.log(`  modified : ${modified.length}`);
  console.log(`  missing  : ${missing.length}`);
  console.log(`  added    : ${added.length}`);
  console.log(`  manifest : ${masterValid ? "valid" : "tampered"}`);

  return masterValid && modified.length === 0 && missing.length === 0 && added.length === 0;
}

function runDemo() {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "hmac-integrity-"));
  fs.writeFileSync(path.join(root, "alpha.txt"), "alpha\n", "utf8");
  fs.writeFileSync(path.join(root, "beta.txt"), "beta\n", "utf8");
  fs.mkdirSync(path.join(root, "nested"), { recursive: true });
  fs.writeFileSync(path.join(root, "nested", "gamma.bin"), crypto.randomBytes(128));

  const secret = "demo-secret";
  const manifestPath = path.join(root, "integrity_manifest.json");

  console.log(`Demo directory: ${root}`);
  generateMode(root, secret, { output: manifestPath, algorithm: "sha-256" });
  fs.appendFileSync(path.join(root, "alpha.txt"), "tampered\n", "utf8");
  fs.unlinkSync(path.join(root, "beta.txt"));
  fs.writeFileSync(path.join(root, "new.txt"), "new file\n", "utf8");
  const ok = verifyMode(root, secret, { output: manifestPath, algorithm: "sha-256" });
  console.log(`Demo verification result: ${ok ? "PASS" : "FAIL (expected for demo)"}`);
}

function usage() {
  return [
    "Usage:",
    "  node src/index.js <generate|verify> <path> <secret-key> [--algorithm SHA-256|SHA-384|SHA-512|SHA3-256]",
    "  [--output integrity_manifest.json] [--exclude \"*.log,*.tmp\"] [--incremental]",
    "",
    "No arguments runs a demo workflow.",
  ].join("\n");
}

function main() {
  const { options, positional } = parseArgs(process.argv.slice(2));
  if (positional.length === 0) {
    runDemo();
    return;
  }
  if (positional.length < 3) throw new Error(usage());

  const mode = String(positional[0]).toLowerCase();
  const targetPath = path.resolve(positional[1]);
  const secret = positional[2];

  if (mode === "generate") {
    generateMode(targetPath, secret, options);
    return;
  }
  if (mode === "verify") {
    const ok = verifyMode(targetPath, secret, options);
    if (!ok) process.exitCode = 2;
    return;
  }
  throw new Error(`Unknown mode: ${mode}\n${usage()}`);
}

try {
  main();
} catch (error) {
  const message = error instanceof Error ? error.message : String(error);
  console.error(`Error: ${message}`);
  process.exit(1);
}