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: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
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, 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

Expand Down
2 changes: 1 addition & 1 deletion src/discover/package-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export async function discoverPackageScripts(root: string): Promise<CommandSourc

for (const filePath of packageFiles) {
const manifest = await readJson<PackageJson>(filePath);
if (!manifest?.scripts) {
if (manifest?.scripts === undefined) {
continue;
}

Expand Down
7 changes: 5 additions & 2 deletions src/discover/taskfiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
34 changes: 34 additions & 0 deletions tests/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down Expand Up @@ -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,
Expand Down