Conversation
|
Two things measured by running The new emitter appends without asking whether the existing PE3 loop already matched that line. Separately, |
c76f7f2 to
585888c
Compare
rng1995
left a comment
There was a problem hiding this comment.
[SkillSpector Review]
Reviewed current head f850169695b2c11875c574df466a9d8134eec95b. The literal single-line os.path.join('/etc', 'passwd') case is detected and every hosted check passes, but the added recognizer is not yet safe to merge.
First, the new emitter independently appends PE3 after the existing PE3 loop, so a line such as os.path.join('.ssh/id_rsa', 'x') produces two HIGH/0.9 findings for the same line and can inflate scoring. Second, [^()\n]+ makes the feature disappear as soon as an otherwise identical call is formatted across lines; imported/aliased join forms are also outside the advertised construction coverage.
Deduplicate/choose the best PE3 finding per source occurrence and use the shared Python AST to resolve literal join calls without reparsing (or consume the cached tree supplied by the analyzer path). Add duplicate, multiline, and supported alias/import regressions. The inline comments identify both defects.
f241d11 to
9d365ed
Compare
rng1995
left a comment
There was a problem hiding this comment.
[SkillSpector Review]
Re-reviewed current head 9d365eded87f5dae23bf4a3225b35556d7b8f0ba against both prior findings, the complete analyzer/test diff, surrounding PE3 aggregation, and exact-head checks.
Neither required correction is present in the current diff. The recognizer is still a single-line os.path.join(...) regex, so multiline and supported imported/aliased forms remain uncovered. It also still appends a second PE3 independently of the existing PE3 loop, allowing one source occurrence to produce duplicate HIGH findings and inflate scoring. Resolve literal joins through the shared cached AST (or an equivalently complete bounded representation), deduplicate per occurrence, and add multiline/alias/duplicate regressions. I have not duplicated the existing inline findings.
All six exact-head checks pass, but the unresolved coverage and scoring regressions plus active change requests block merging.
Priority: P1 — duplicated HIGH findings and missed path constructions directly affect risk scoring.
Signed-off-by: Deepak Jain <deepujain@gmail.com>
Signed-off-by: Deepak Jain <deepujain@gmail.com>
9d365ed to
1782279
Compare
|
All review comments are addressed on the current head
CI is green on this head: https://github.com/NVIDIA/SkillSpector/actions/runs/35404568143 (lint, test-unit, docker-smoke, DCO). Per your 2026-09-18 note, this PR is ready for re-review. |
The PE3 constructed-path recognizer missed multiline and imported/aliased os.path.join spellings, and emitted a second PE3 for lines the pattern loop already covered. Resolve literal join calls from the Python AST (handling from/import aliases), and keep the best-confidence PE3 per line. Signed-off-by: Deepak Jain <deepujain@gmail.com>
|
Addressed both open P1 threads in 8262737:
CI is re-running on this head. |
The AST-based os.path.join resolver parsed every Python file, defeating the runner's parse-once invariant (one shared parse per file across the analyzer fan-out). Gate the local parse behind a cheap textual pre-check so files without a plausible join() call never pay for it. The module stays lexical, preserving windowed and normalized-view scans for all file types. Signed-off-by: Deepak Jain <deepujain@gmail.com>
|
Follow-up: 8262737 broke the parse-once invariant (test_graph_scan_parses_python_once_before_parallel_analyzers) because the AST resolver parsed every Python file unconditionally. 40cea88 gates the local parse behind a cheap join() pre-check so files without a plausible join call never parse; the module stays lexical, preserving windowed and normalized-view scans for all file types. The 329 pattern/runner tests pass locally. CI is re-running on this head. |
rng1995
left a comment
There was a problem hiding this comment.
[SkillSpector Review]
Re-reviewed current head 40cea883f8019ff0db6a15edd5b7f916aac1ab4f, including the complete two-file diff, both previous review threads and author replies, the shared AST/parser and static-runner integration, the new regression assertions, and current CI.
The original single-line duplicate and the explicit multiline/direct-import examples are addressed. Both earlier threads are marked resolved, but the implementation does not yet satisfy the full corrections:
- A renamed import (
from os.path import join as j) is rejected by the new textual precheck before alias resolution runs. - Wrapping the duplicate example across lines still emits both the raw-literal and constructed-path findings because deduplication compares only the call's starting line.
- Files containing a plausible join still call
parse_python_sourcedirectly, bypassing the prewarmed cache. The current parse-once graph fixture contains no join, so green CI does not cover this remainder.
The inline comments give the remaining cases and expected regression coverage. All six reported CI checks pass; gh pr checks --required reports no required checks. Tests were inspected, not executed locally. Changes remain required; no approval or merge is submitted.
| left to the existing pattern loop. Unparseable content simply yields no | ||
| findings here. | ||
| """ | ||
| if not _JOIN_CALL_HINT.search(content): |
There was a problem hiding this comment.
[P1] Let renamed join imports reach alias resolution. from os.path import join as j\ntarget = j('/etc', 'passwd')\n contains no join(, so this precheck returns before resolve_call_name can map j to os.path.join. The credential path is split across literals, so the lexical PE3 loop cannot recover it either. Remove this spelling-dependent gate or make it cover imported local names, and add a renamed-import regression alongside the existing direct-import cases.
There was a problem hiding this comment.
Done: the pre-check is now built from the import aliases, so every local name bound to os.path or os.path.join (e.g. j from from os.path import join as j) counts as a plausible join call before alias resolution runs. Regression: test_pe3_aliased_join_imports_are_detected now includes the renamed-import case.
| """ | ||
| if not _JOIN_CALL_HINT.search(content): | ||
| return [] | ||
| parsed = parse_python_source(content, file_path) |
There was a problem hiding this comment.
[P2] Reuse the scan's parsed tree for files that contain join calls. parse_python_source calls ast.parse unconditionally; it does not consult the runtime cache. This module also lacks the runner's shared-AST hook, so the textual gate only preserves parse-once behavior for files with no plausible join. Route this supplemental analysis through the cached AST (while retaining the lexical/windowed scans), and extend test_graph_scan_parses_python_once_before_parallel_analyzers with a literal os.path.join call so it verifies that the graph still performs one parse.
There was a problem hiding this comment.
Done: the module stays lexical so the windowed and normalized views keep running, and the constructed-path analysis now reuses the scan shared parse via the new peek_python_ast helper (exact-content hit only, never parses or writes), published to analyze() by node() through a contextvar. Standalone callers keep one on-demand parse. The extended test_graph_scan_parses_python_once_before_parallel_analyzers now includes a literal os.path.join call and still asserts a single parse.
| ( | ||
| existing | ||
| for existing in findings | ||
| if existing.rule_id == "PE3" and existing.location.start_line == line_num |
There was a problem hiding this comment.
[P1] Deduplicate the whole call occurrence, including wrapped arguments. With import os\np = os.path.join(\n '.ssh/id_rsa', 'x'\n)\n, the lexical PE3 result starts on line 3, while the constructed result is anchored to node.lineno on line 2. This equality therefore misses the existing finding and appends a second HIGH/0.9 result for the same credential-path occurrence. Carry the call's source span and match raw findings belonging to that span, preserving unrelated occurrences; add a multiline version of the duplicate regression.
There was a problem hiding this comment.
Done: constructed hits now carry the call (start_line, end_line) span, and dedup matches raw PE3 findings anywhere inside that span, so a wrapped join call yields one PE3 while unrelated occurrences on other lines are preserved. Regression: test_pe3_multiline_constructed_join_is_deduplicated.
Address rng1995's review on constructed sensitive-path detection:
- Broaden the join pre-check to cover imported local names: the gate now
matches every name the file binds to os.path or os.path.join, so
'from os.path import join as j' followed by j('/etc', 'passwd') reaches
alias resolution instead of returning early on the missing literal join(.
- Route the supplemental analysis through the scan's shared AST: node()
publishes the runner's python_ast_cache_key via a contextvar and
_constructed_sensitive_paths consults it with the new peek_python_ast
helper (exact-content hit only, never parses or writes). The module stays
lexical so windowed and normalized views keep running; windowed view
fragments are skipped and standalone callers keep one on-demand parse.
- Deduplicate across the call's whole source span: hits now carry
(start_line, end_line), so a join call wrapped over several lines no
longer produces a second PE3 when the lexical loop fires on a wrapped
argument line.
Tests: extend test_pe3_aliased_join_imports_are_detected with the renamed
import case, add test_pe3_multiline_constructed_join_is_deduplicated, and
extend test_graph_scan_parses_python_once_before_parallel_analyzers with a
literal os.path.join call to verify the graph still performs one parse.
Signed-off-by: Deepak Jain <deepujain@gmail.com>
|
Confirmed on 56d121c — the alias-aware gate resolves the renamed import, and a file whose only join is |
Above SECURITY_VIEW_WINDOW_CHARS the runner hands lexical modules window slices instead of the whole file, so peek_python_ast never hits the scan's whole-file cache entry and the constructed-path analysis silently dropped its findings (regression introduced in 56d121c; 40cea88 reported them). On a peek miss, parse the provided content directly behind the same plain textual join gate, restoring the 40cea88 behavior for window slices while keeping the 17:45 improvements for whole files: alias-aware gate, shared AST reuse, multiline span dedup, and scan-count discipline (fragments without a plausible join call never parse). Regression tests: - unit: a windowed fragment under a scan keeps PE3 coverage, and a join-free fragment triggers no parse - graph: a ~376k-char file keeps its constructed-path PE3 through the full scan Signed-off-by: Deepak Jain <deepujain@gmail.com>
|
Replying to the regression thread (#545 (comment)): fixed in c8d201b. Root cause: above SECURITY_VIEW_WINDOW_CHARS the runner hands lexical modules window slices instead of the whole file, so peek_python_ast never matches the scan's whole-file cache entry, and the constructed-path analysis returned [] for every windowed fragment. That was the silent drop you measured. Fix: on a peek miss, parse the provided fragment directly behind the same plain textual join gate (restoring the 40cea88 behavior for window slices), while keeping the 17:45 improvements for whole files: alias-aware gate, shared AST reuse, multiline span dedup, and no parse for join-free fragments. Verification:
|
Fixes #477.
Resolve literal-only
os.path.join(...)calls before matching PE3 credential paths, preserving the detector for constructed paths without evaluating arbitrary expressions.Validation:
python3 -m py_compile src/skillspector/nodes/analyzers/static_patterns_privilege_escalation.py tests/unit/test_patterns.pyandgit diff --check. The isolated checkout lacks the project test environment; hosted CI will run the full suite.