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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ For a fixture-backed walkthrough, see [`docs/tutorials/audit-agent-cli-scripts.m
- Markdown shell blocks fenced with backticks or tildes and labelled `bash`, `sh`, `shell`, `console`, or `zsh` (unlabelled fences are also scanned). Each independently executable line must 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`).

Make discovery recognizes ordinary named targets, including rules that list
multiple targets; each target is reported at the shared rule location with the
same recipe. Pattern and special dot-prefixed targets are intentionally skipped.
Justfile discovery recognizes named recipes with optional parameters and
defaults. It ignores settings and assignments, and reports the recipe name and
indented command body without expanding parameter values.

Markdown blocks labelled with other languages are deliberately ignored. Prose, comments, command output, continuations that do not start with a supported command, and commands embedded later in a line are not treated as independently executable commands.

## Risk Model
Expand Down
5 changes: 5 additions & 0 deletions examples/fixtures/docs-only/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ clean:
publish-inline: ; npm publish

verify-inline: package.json ; npm test

build test: package.json
npm test

export MODE := test
7 changes: 7 additions & 0 deletions examples/fixtures/docs-only/justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
set shell := ["bash", "-cu"]

deploy environment:
rm -rf "build/{{environment}}"

verify suite="unit":
npm test -- "{{suite}}"
30 changes: 18 additions & 12 deletions src/discover/makefile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ export async function discoverMakeTargets(root: string): Promise<CommandSource[]

for (let index = 0; index < lines.length; index += 1) {
const line = lines[index];
const match = /^(?!\t|\s)([A-Za-z0-9_.-]+):(.*)$/.exec(line);
if (!match || match[1].startsWith(".")) {
const match = /^([^:=\s][^:=]*):(?![=])(.*)$/.exec(line);
if (!match) {
continue;
}
const targets = match[1].trim().split(/\s+/);
if (targets.some((target) => !/^[A-Za-z0-9_.-]+$/.test(target) || target.startsWith("."))) {
continue;
}

Expand All @@ -38,16 +42,18 @@ export async function discoverMakeTargets(root: string): Promise<CommandSource[]
}

if (body.length > 0) {
commands.push({
id: `${relativeFile}#${match[1]}`,
name: match[1],
command: body.join(" && "),
kind: "makefile",
location: {
file: relativeFile,
line: index + 1
}
});
for (const target of targets) {
commands.push({
id: `${relativeFile}#${target}`,
name: target,
command: body.join(" && "),
kind: "makefile",
location: {
file: relativeFile,
line: index + 1
}
});
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/discover/taskfiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function discoverJustfile(file: string, text: string): CommandSource[] {
const commands: CommandSource[] = [];
const lines = text.split(/\r?\n/);
for (let index = 0; index < lines.length; index += 1) {
const match = /^([A-Za-z0-9_-]+):/.exec(lines[index]);
const match = /^([A-Za-z0-9_-]+)(?:\s+[^:=\s][^:]*)?:\s*(?:#.*)?$/.exec(lines[index]);
if (!match) {
continue;
}
Expand Down
10 changes: 10 additions & 0 deletions tests/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ test("CLI supports PRD fail-on high alias", () => {
assert.match(result.stderr, /Risk threshold met: high/);
});

test("CLI fail-on detects dangerous parameterized Just recipes", () => {
const result = spawnSync(process.execPath, [cli, "scan", "examples/fixtures/docs-only", "--format", "json", "--fail-on", "dangerous"], {
cwd: repo,
encoding: "utf8"
});
assert.equal(result.status, 1);
assert.match(result.stderr, /Risk threshold met: dangerous/);
assert.equal(JSON.parse(result.stdout).commands.some(({ kind, name }) => kind === "justfile" && name === "deploy"), true);
});

test("CLI rejects a missing explicitly requested config", () => {
const result = spawnSync(process.execPath, [cli, "scan", "examples/fixtures/clean", "--config", "missing.config.json"], {
cwd: repo,
Expand Down
23 changes: 23 additions & 0 deletions tests/scan.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,26 @@ test("scans same-line Makefile recipes with and without prerequisites", async ()
assert.equal(makeCommands.get("verify-inline")?.location.line, 9);
assert.equal(makeCommands.get("validate")?.command, "npm run check");
});

test("scans multi-target Make rules and parameterized Just recipes", async () => {
const report = await scanProject({ root: fixture("docs-only") });
const commands = new Map(report.commands.map((command) => [`${command.kind}:${command.name}`, command]));

for (const name of ["build", "test"]) {
const command = commands.get(`makefile:${name}`);
assert.equal(command?.command, "npm test");
assert.equal(command?.location.file, "Makefile");
assert.equal(command?.location.line, 11);
assert.equal(command?.risk, "safe");
}
assert.equal(commands.has("makefile:export"), false);

const deploy = commands.get("justfile:deploy");
assert.equal(deploy?.command, 'rm -rf "build/{{environment}}"');
assert.equal(deploy?.location.file, "justfile");
assert.equal(deploy?.location.line, 3);
assert.equal(deploy?.risk, "dangerous");
assert.equal(commands.get("justfile:verify")?.command, 'npm test -- "{{suite}}"');
assert.equal(commands.get("justfile:verify")?.risk, "safe");
assert.equal(commands.has("justfile:set"), false);
});