Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
### 2.12.0 (Unreleased)
### Features/Bug Fixes
* fix(pi): accept scans from symlinked workspace roots while preserving the CLI's checks on symlinks below the workspace. Custom YARA rules loaded with `--yara-rules-dir` cannot use `include`; place each rule directly in the selected directory.
* fix(taint): key TT1-TT6 taint by lexical scope and follow direct calls to functions and methods defined in the same file, so a tainted variable no longer taints same-named variables in other functions (#799). Message-glob baseline rules keep matching TT findings whose rule and message are unchanged; exact baseline fingerprints are bound to the scanner version and suppress nothing after an upgrade (see docs/SUPPRESSION.md), so they must be regenerated in any case. A TT message changes where the earlier message came from a same-named variable in another scope (it now names the variable, source and line that reach the sink); where a more severe source raises the rule (TT2 to TT3 or TT4, TT4 to TT3, TT2 to TT5 or TT6), and at such a sink, where a weaker finding is subsumed or now cites another variable; and where an identifier longer than 120 characters is now shortened. Re-accept those findings or regenerate the baseline. `match_fingerprint` changes only when the rule does.
* fix(static): stop code comparisons after removal verbs from reading as unterminated tag-marker directives (#777)
* fix(baseline): fingerprint every occurrence of a deduplicated finding so the next scan suppresses all of them (#633)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -782,7 +782,7 @@ Options:
-f, --format [terminal|json|markdown|sarif] Output format [default: terminal]
-o, --output PATH Output file path
--no-llm Skip LLM analysis (static only)
--yara-rules-dir PATH Extra YARA rules directory
--yara-rules-dir PATH Extra YARA rules directory (includes disabled)
-b, --baseline PATH Suppress findings listed in a baseline
--show-suppressed List baseline-suppressed findings
-V, --verbose Show detailed progress
Expand Down
10 changes: 7 additions & 3 deletions extensions/skillspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,13 @@ async function approveScanInputs(
if (!isWithin(resolve(ctx.cwd), path.input) && !isWithin(workspace, path.resolved) && path.resolved !== path.input) {
aliasRequests.push(readRequest(path.field, path.resolved));
}
// Keep the original target path so the CLI can enforce its no-symlink
// input policy. YARA directories are canonicalised by the CLI too.
prepared[path.field] = path.field === "target" ? path.input : path.resolved;
// Resolve only the workspace prefix; keep child symlinks for the CLI's
// no-symlink input checks. YARA directories are canonicalised by the CLI too.
prepared[path.field] = path.field === "target"
? isWithin(resolve(ctx.cwd), path.input)
? join(workspace, relative(resolve(ctx.cwd), path.input))
: path.input
: path.resolved;
}
await approve(aliasRequests);
signal?.throwIfAborted();
Expand Down
39 changes: 39 additions & 0 deletions tests/unit/test_pi_extension.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,23 @@ test("uses installed absolute executable and preserves scan arguments without ou
assert.equal(ctx.calls[0].options.cwd, ctx.workspace);
});

test("accepts a symlinked workspace without resolving target symlinks below it", async (t) => {
const ctx = await setup(t);
const alias = join(ctx.root, "workspace-alias");
symlinkSync(ctx.workspace, alias);
symlinkSync(join(ctx.workspace, "SKILL.md"), join(ctx.workspace, "linked.md"));
for (const [target, expected] of [
["./SKILL.md", join(ctx.workspace, "SKILL.md")],
[".", ctx.workspace],
[join(alias, "SKILL.md"), join(ctx.workspace, "SKILL.md")],
["linked.md", join(ctx.workspace, "linked.md")],
]) {
await ctx.scan({ target }, { cwd: alias, hasUI: false });
assert.equal(ctx.calls.at(-1).args[1], expected);
}
assert.equal(ctx.prompts.length, 0);
});

test("redacts long scanner output without retrying every word character", { timeout: 5000 }, async (t) => {
const ctx = await setup(t, () => ({
code: 0,
Expand Down Expand Up @@ -209,6 +226,28 @@ test("approves canonical external scope and preserves the CLI target symlink pol
assert.equal(ctx.calls[0].args.at(-1), join(ctx.root, "rules"));
});

for (const approveResolved of [true, false]) {
test(`external YARA aliases require a second confirmation: approved=${approveResolved}`, async (t) => {
let confirmations = 0;
const ctx = await setup(t, undefined, async () => ++confirmations === 1 || approveResolved);
const rules = join(ctx.root, "rules");
const alias = join(ctx.root, "rules-alias");
mkdirSync(rules);
symlinkSync(rules, alias);
const scan = ctx.scan({ yaraRulesDir: "../rules-alias" });
if (approveResolved) {
await scan;
assert.equal(ctx.calls[0].args.at(-1), rules);
} else {
await assert.rejects(scan, /not approved/);
assert.equal(ctx.calls.length, 0);
}
assert.equal(ctx.prompts.length, 2);
assert.ok(ctx.prompts[0].message.includes(JSON.stringify(alias)));
assert.ok(ctx.prompts[1].message.includes(JSON.stringify(rules)));
});
}

test("treats local git@ paths as canonical local reads", async (t) => {
const ctx = await setup(t, undefined, async () => true);
mkdirSync(join(ctx.workspace, "git@notes"));
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_suppression.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ def test_transitive_fingerprint_does_not_borrow_lookalike_local_path(delimiter:
def test_root_lookup_does_not_normalize_a_transitive_content_key() -> None:
from skillspector.suppression import _component_content

key = source_content_key("scope", r"..\..\..\x")
key = source_content_key(f"external/{'a' * 64}", r"..\..\..\x")
assert _component_content({key: "remote content"}, './x"]') is None


Expand Down