Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
a50d5a6
fix: preserve Pi workspace aliases and strengthen security regressions
yashrajp22 Oct 14, 2026
b686cd5
Merge branch 'main' into yashraj/fix-merged-review-regressions
github-actions[bot] Oct 14, 2026
6187757
Merge branch 'main' into yashraj/fix-merged-review-regressions
github-actions[bot] Oct 14, 2026
010da6e
Merge branch 'main' into yashraj/fix-merged-review-regressions
github-actions[bot] Oct 14, 2026
eb5e9d9
Merge branch 'main' into yashraj/fix-merged-review-regressions
github-actions[bot] Oct 14, 2026
54d492d
Merge branch 'main' into yashraj/fix-merged-review-regressions
github-actions[bot] Oct 14, 2026
48c044a
Merge branch 'main' into yashraj/fix-merged-review-regressions
github-actions[bot] Oct 14, 2026
f3b8e8a
Merge branch 'main' into yashraj/fix-merged-review-regressions
github-actions[bot] Oct 15, 2026
509ac09
Merge branch 'main' into yashraj/fix-merged-review-regressions
github-actions[bot] Oct 15, 2026
415f722
Merge current main while preserving workspace alias checks
yashrajp22 Nov 10, 2026
66852ef
Merge preceding scanner updates
yashrajp22 Nov 11, 2026
73bb5d4
Merge branch 'main' into yashraj/fix-merged-review-regressions
github-actions[bot] Nov 11, 2026
3dd55ba
Merge branch 'main' into yashraj/fix-merged-review-regressions
github-actions[bot] Nov 11, 2026
17692e4
Merge branch 'main' into yashraj/fix-merged-review-regressions
github-actions[bot] Nov 11, 2026
e966ac9
Merge branch 'main' into yashraj/fix-merged-review-regressions
github-actions[bot] Nov 11, 2026
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(coverage): account for successfully inspected linked ZIP containers while retaining member failures and archive limits
* fix(static): stop Markdown code fence lines from reading as an open quote that merges consecutive shell lines into one marker directive (#868)
* fix(ra1): treat a prohibition on recommending a self-update ("Do not recommend self-update") as the prohibition it is, not as an RA1 self-modification instruction (#928)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -813,7 +813,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
Loading