diff --git a/README.md b/README.md index 1e07f27..1e33640 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ An explicitly supplied `--config` path must exist. Config files are validated be ScriptAudit is a static heuristic tool, not a shell sandbox. It does not prove that a command is safe, and it does not replace maintainer judgment. Treat reports as a review appendix before running commands in an unfamiliar repo. -Discovery is fail-closed for invalid command sources. Malformed `package.json` or Taskfile YAML, non-string `package.json` script values, and unsupported or malformed Taskfile `cmds` entries stop the scan with a nonzero exit and a path-specific diagnostic; they are never treated as a clean zero-command audit. +Discovery is fail-closed for invalid command sources. Malformed `package.json` or Taskfile YAML, present non-object `scripts` or `tasks` containers, non-object Taskfile task definitions, non-string `package.json` script values, and unsupported or malformed Taskfile `cmds` entries stop the scan with a nonzero exit and a path-specific diagnostic; they are never treated as a clean zero-command audit. An omitted `scripts` or `tasks` field is valid and contributes no commands. ## Agent Workflow diff --git a/src/discover/package-json.ts b/src/discover/package-json.ts index 23f19dd..313734f 100644 --- a/src/discover/package-json.ts +++ b/src/discover/package-json.ts @@ -13,7 +13,7 @@ export async function discoverPackageScripts(root: string): Promise(filePath); - if (!manifest?.scripts) { + if (manifest?.scripts === undefined) { continue; } diff --git a/src/discover/taskfiles.ts b/src/discover/taskfiles.ts index 97e8521..9f4dd70 100644 --- a/src/discover/taskfiles.ts +++ b/src/discover/taskfiles.ts @@ -54,12 +54,15 @@ function discoverTaskfile(file: string, text: string): CommandSource[] { } catch { throw new Error(`Invalid Taskfile YAML in ${file}.`); } - if (!isRecord(document) || !isRecord(document.tasks)) { + if (!isRecord(document) || document.tasks === undefined) { return commands; } + if (!isRecord(document.tasks)) { + throw new Error(`Invalid Taskfile tasks in ${file}: tasks must be an object.`); + } for (const [name, task] of Object.entries(document.tasks)) { if (!isRecord(task)) { - continue; + throw new Error(`Invalid Taskfile task in ${file} at tasks.${name}: expected an object.`); } const body = taskCommands(task.cmds, file, name); if (body) { diff --git a/tests/cli.test.mjs b/tests/cli.test.mjs index 07cf1b6..3cfa3a5 100644 --- a/tests/cli.test.mjs +++ b/tests/cli.test.mjs @@ -97,6 +97,23 @@ test("CLI rejects non-string package scripts with a path-specific diagnostic", ( assert.equal(result.stderr, "Invalid package script in package.json at scripts.test: expected a string.\n"); }); +test("CLI rejects a null package scripts container instead of reporting zero commands", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "scriptaudit-null-scripts-")); + writeFileSync(path.join(root, "package.json"), '{"name":"probe","scripts":null}'); + const result = scan(root); + assert.equal(result.status, 1); + assert.equal(result.stdout, ""); + assert.equal(result.stderr, "Invalid package scripts in package.json: scripts must be an object of string values.\n"); +}); + +test("CLI accepts an absent package scripts container", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "scriptaudit-absent-scripts-")); + writeFileSync(path.join(root, "package.json"), '{"name":"probe"}'); + const result = scan(root); + assert.equal(result.status, 0, result.stderr); + assert.equal(JSON.parse(result.stdout).summary.total, 0); +}); + test("CLI rejects malformed package JSON with the discovered path", () => { const root = mkdtempSync(path.join(os.tmpdir(), "scriptaudit-invalid-json-")); mkdirSync(path.join(root, "nested")); @@ -145,6 +162,23 @@ test("CLI rejects malformed Taskfile command entries with a path-specific diagno ); }); +test("CLI rejects a non-object Taskfile task instead of reporting zero commands", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "scriptaudit-invalid-task-")); + writeFileSync(path.join(root, "Taskfile.yml"), "version: '3'\ntasks:\n broken: npm test\n"); + const result = scan(root); + assert.equal(result.status, 1); + assert.equal(result.stdout, ""); + assert.equal(result.stderr, "Invalid Taskfile task in Taskfile.yml at tasks.broken: expected an object.\n"); +}); + +test("CLI accepts an absent Taskfile tasks container", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "scriptaudit-absent-tasks-")); + writeFileSync(path.join(root, "Taskfile.yml"), "version: '3'\n"); + const result = scan(root); + assert.equal(result.status, 0, result.stderr); + assert.equal(JSON.parse(result.stdout).summary.total, 0); +}); + function scan(root) { return spawnSync(process.execPath, [cli, "scan", root, "--format", "json"], { cwd: repo,