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);
}