Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
27 changes: 20 additions & 7 deletions src/discover/taskfiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand All @@ -79,18 +79,31 @@ function isRecord(value: unknown): value is Record<string, unknown> {
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, "\\$&");
Expand Down
29 changes: 29 additions & 0 deletions tests/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down