From e862e054ae92837a74d80f3cfa4f8d82ea52d297 Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Sun, 6 Sep 2026 19:39:50 +1000 Subject: [PATCH 1/3] test: cover deferred Taskfile commands --- tests/cli.test.mjs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/cli.test.mjs b/tests/cli.test.mjs index 0817a23..07cf1b6 100644 --- a/tests/cli.test.mjs +++ b/tests/cli.test.mjs @@ -116,6 +116,35 @@ test("CLI rejects malformed Taskfile YAML instead of reporting zero commands", ( assert.equal(result.stderr, "Invalid Taskfile YAML in Taskfile.yml.\n"); }); +test("CLI includes deferred Taskfile commands in task risk evidence", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "scriptaudit-taskfile-defer-")); + writeFileSync( + path.join(root, "Taskfile.yml"), + "version: '3'\ntasks:\n deploy:\n cmds:\n - cmd: npm test\n - defer: rm -rf build\n" + ); + const result = scan(root); + assert.equal(result.status, 0, result.stderr); + const report = JSON.parse(result.stdout); + assert.equal(report.commands[0].command, "npm test && rm -rf build"); + assert.equal(report.commands[0].risk, "dangerous"); + assert.equal(report.summary.dangerous, 1); +}); + +test("CLI rejects malformed Taskfile command entries with a path-specific diagnostic", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "scriptaudit-invalid-task-command-")); + writeFileSync( + path.join(root, "Taskfile.yml"), + "version: '3'\ntasks:\n deploy:\n cmds:\n - cmd: npm test\n - defer: false\n" + ); + const result = scan(root); + assert.equal(result.status, 1); + assert.equal(result.stdout, ""); + assert.equal( + result.stderr, + "Invalid Taskfile command in Taskfile.yml at tasks.deploy.cmds[1]: expected a string, cmd string, or defer string.\n" + ); +}); + function scan(root) { return spawnSync(process.execPath, [cli, "scan", root, "--format", "json"], { cwd: repo, From b26947f2e0782fca4319194012fb206d8404e2af Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Sun, 6 Sep 2026 19:40:39 +1000 Subject: [PATCH 2/3] fix: audit deferred Taskfile commands --- src/discover/taskfiles.ts | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/discover/taskfiles.ts b/src/discover/taskfiles.ts index 034d343..97e8521 100644 --- a/src/discover/taskfiles.ts +++ b/src/discover/taskfiles.ts @@ -61,7 +61,7 @@ function discoverTaskfile(file: string, text: string): CommandSource[] { if (!isRecord(task)) { continue; } - const body = taskCommands(task.cmds); + const body = taskCommands(task.cmds, file, name); if (body) { commands.push({ id: `${file}#${name}`, @@ -79,18 +79,31 @@ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } -function taskCommands(value: unknown): string { +function taskCommands(value: unknown, file: string, taskName: string): string { + if (value === undefined) return ""; const entries = Array.isArray(value) ? value : [value]; return entries - .map((entry) => { - if (typeof entry === "string") return entry.trim(); - if (isRecord(entry) && typeof entry.cmd === "string") return entry.cmd.trim(); - return ""; + .map((entry, index) => { + const command = taskCommand(entry); + if (command) return command; + const entryPath = Array.isArray(value) + ? `tasks.${taskName}.cmds[${index}]` + : `tasks.${taskName}.cmds`; + throw new Error( + `Invalid Taskfile command in ${file} at ${entryPath}: expected a string, cmd string, or defer string.` + ); }) - .filter(Boolean) .join(" && "); } +function taskCommand(entry: unknown): string { + if (typeof entry === "string") return entry.trim(); + if (!isRecord(entry)) return ""; + if (typeof entry.cmd === "string") return entry.cmd.trim(); + if (typeof entry.defer === "string") return entry.defer.trim(); + return ""; +} + function taskLine(text: string, taskName: string): number | undefined { const lines = text.split(/\r?\n/); const escapedName = taskName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); From 73f99fb442f5d12d77de1ed5d640993ca97527e6 Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Sun, 6 Sep 2026 19:41:09 +1000 Subject: [PATCH 3/3] docs: define Taskfile command discovery --- CHANGELOG.md | 2 ++ README.md | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7ce193..454d22c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ format and uses semantic versioning when versioned releases are published. ### Fixed +- Include Taskfile deferred-command mappings in audit evidence and reject malformed command entries with path-specific diagnostics. + - Join shell continuations and recognize leading environment assignments when discovering commands in Markdown fences. diff --git a/README.md b/README.md index 3812f63..1e07f27 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ For a fixture-backed walkthrough, see [`docs/tutorials/audit-agent-cli-scripts.m - `pnpm-workspace.yaml` workspace hints. - Makefile targets. - Markdown shell blocks fenced with backticks or tildes and labelled `bash`, `sh`, `shell`, `console`, or `zsh` (unlabelled fences are also scanned). Shell lines ending in `\` are joined before classification, and commands may begin with standard environment assignments such as `CI=1 npm test`. Each independently executable command must otherwise begin with a supported command or an optional `$` prompt. Supported commands include package and task runners, Node and shell entry points, plus risk-relevant network, container, version-control, destructive, permission, deploy, and publish tools such as `curl`, `docker`, `git`, `rm`, `sudo`, and `vercel`. -- Justfile recipes and Taskfile `cmds` entries written as scalar commands (`- npm test`) or inline mappings (`- cmd: npm test`). +- Justfile recipes and Taskfile `cmds` entries written as scalar commands (`- npm test`), command mappings (`- cmd: npm test`), or deferred-command mappings (`- defer: rm -rf build`). Deferred commands are included in the task's compound command and risk evidence. Make discovery recognizes ordinary named targets, including rules that list multiple targets; each target is reported at the shared rule location with the @@ -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 and non-string `package.json` script values 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, 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. ## Agent Workflow