diff --git a/README.md b/README.md index 96f8e12..2a5c314 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/examples/fixtures/docs-only/Makefile b/examples/fixtures/docs-only/Makefile index 90fc14b..710b31d 100644 --- a/examples/fixtures/docs-only/Makefile +++ b/examples/fixtures/docs-only/Makefile @@ -7,3 +7,8 @@ clean: publish-inline: ; npm publish verify-inline: package.json ; npm test + +build test: package.json + npm test + +export MODE := test diff --git a/examples/fixtures/docs-only/justfile b/examples/fixtures/docs-only/justfile new file mode 100644 index 0000000..d984ae1 --- /dev/null +++ b/examples/fixtures/docs-only/justfile @@ -0,0 +1,7 @@ +set shell := ["bash", "-cu"] + +deploy environment: + rm -rf "build/{{environment}}" + +verify suite="unit": + npm test -- "{{suite}}" diff --git a/src/discover/makefile.ts b/src/discover/makefile.ts index 744e721..849470b 100644 --- a/src/discover/makefile.ts +++ b/src/discover/makefile.ts @@ -17,8 +17,12 @@ export async function discoverMakeTargets(root: string): Promise !/^[A-Za-z0-9_.-]+$/.test(target) || target.startsWith("."))) { continue; } @@ -38,16 +42,18 @@ export async function discoverMakeTargets(root: string): Promise 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 + } + }); + } } } } diff --git a/src/discover/taskfiles.ts b/src/discover/taskfiles.ts index b11d822..a2c7bfe 100644 --- a/src/discover/taskfiles.ts +++ b/src/discover/taskfiles.ts @@ -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; } diff --git a/tests/cli.test.mjs b/tests/cli.test.mjs index 3613334..2e3646e 100644 --- a/tests/cli.test.mjs +++ b/tests/cli.test.mjs @@ -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, diff --git a/tests/scan.test.mjs b/tests/scan.test.mjs index b5ffb64..1f20383 100644 --- a/tests/scan.test.mjs +++ b/tests/scan.test.mjs @@ -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); +});