Skip to content

Commit 4274da6

Browse files
authored
Multiple fixes to improve detection (#1974)
* Allow vulnerability scans on arbitrary N|Solid refs Known branches still use curated dependency lists, but unknown refs now fall back to probing every tracked dependency definition and keeping only the ones whose version parsers succeed against the checked-out tree. This lets the scanner run against release branches and ad hoc refs without having to pre-register each branch name in dependencies_per_branch. Also document the workflow input as an arbitrary N|Solid branch/ref and mark curated scans as incomplete when a curated dependency fails to resolve. * Harden dependency resolution, npm audit edge cases, and reconciliation Make arbitrary-ref dependency probing more tolerant of parser shape errors and mark curated scans as incomplete when curated dependencies fail to resolve. Tighten npm audit handling by: - recording discovery failures and empty installed bundle trees as incomplete - generating prod-only lockfiles with --package-lock-only when no lockfile is present - retrying ENOLOCK recovery even when node_modules exists without a lockfile - handling boolean bundleDependencies and avoiding duplicate installed-tree walks - skipping invalid GitHub advisory specifiers or versions per advisory while marking the scan incomplete - making per-package GitHub advisory query failures non-fatal - normalizing npm advisory IDs across modern and legacy audit payloads - preferring CVE identifiers when GitHub exposes them Preserve reconciliation when a vendored npm advisory changes from GHSA to CVE by carrying alternate advisory identifiers in the scan payload and matching existing issues through those aliases before create/close decisions.
1 parent ba643e2 commit 4274da6

5 files changed

Lines changed: 1105 additions & 217 deletions

File tree

.github/workflows/check-vulns.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ on:
66
nsolidStream:
77
type: string
88
default: 'main'
9+
description: 'N|Solid branch or ref to scan'
910
secrets:
1011
NVD_API_KEY:
1112
required: true
@@ -14,6 +15,7 @@ on:
1415
nsolidStream:
1516
type: string
1617
default: 'main'
18+
description: 'N|Solid branch or ref to scan'
1719

1820

1921
permissions:

dep_checker/main.py

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class Vulnerability:
3535
def __init__(self, id: str, url: str, dependency: str, version: str, source: str = "binary",
3636
severity: Optional[str] = None, via: Optional[list] = None,
3737
fix_available: Optional[bool] = None, main_dep_name: Optional[str] = None,
38-
main_dep_path: Optional[str] = None):
38+
main_dep_path: Optional[str] = None, advisory_aliases: Optional[list[str]] = None):
3939
self.id = id
4040
self.url = url
4141
self.dependency = dependency
@@ -46,6 +46,7 @@ def __init__(self, id: str, url: str, dependency: str, version: str, source: str
4646
self.fix_available = fix_available # whether fix is available
4747
self.main_dep_name = main_dep_name # main dependency name for npm vulnerabilities
4848
self.main_dep_path = main_dep_path # path to the main dependency
49+
self.advisory_aliases = advisory_aliases or [] # alternate IDs for reconciliation migration
4950

5051

5152
class VulnerabilityEncoder(json.JSONEncoder):
@@ -69,6 +70,8 @@ def default(self, obj):
6970
result["main_dep_path"] = obj.main_dep_path
7071
if obj.fix_available is not None:
7172
result["fix_available"] = obj.fix_available
73+
if obj.advisory_aliases:
74+
result["advisory_aliases"] = obj.advisory_aliases
7275
return result
7376
# Let the base class default method raise the TypeError
7477
return json.JSONEncoder.default(self, obj)
@@ -103,6 +106,64 @@ def default(self, obj):
103106
)
104107

105108

109+
def resolve_dependencies(
110+
repo_path: Path, repo_branch: str
111+
) -> tuple[dict[str, Dependency], list[str]]:
112+
"""Return the dependencies that can be parsed from the checked-out repo.
113+
114+
Known branches still use their curated dependency list. Unknown branches fall
115+
back to probing every tracked dependency definition and keeping only the ones
116+
whose version parser succeeds against the checkout.
117+
"""
118+
119+
configured_names = dependencies_per_branch.get(repo_branch)
120+
if configured_names is None:
121+
candidate_dependencies = dependencies_info
122+
print(
123+
f"Info: '{repo_branch}' is not explicitly configured; scanning all known dependency definitions"
124+
)
125+
else:
126+
candidate_dependencies = {
127+
name: dep
128+
for name, dep in dependencies_info.items()
129+
if name in configured_names
130+
}
131+
132+
available_dependencies: dict[str, Dependency] = {}
133+
skipped_dependencies: list[str] = []
134+
135+
for name, dep in candidate_dependencies.items():
136+
try:
137+
dep.version_parser(repo_path)
138+
except (
139+
FileNotFoundError,
140+
RuntimeError,
141+
ValueError,
142+
KeyError,
143+
IndexError,
144+
AttributeError,
145+
TypeError,
146+
OSError,
147+
) as exc:
148+
skipped_dependencies.append(f"{name}: {exc}")
149+
continue
150+
available_dependencies[name] = dep
151+
152+
if not available_dependencies:
153+
raise RuntimeError(
154+
f"No supported dependencies could be resolved from '{repo_branch}'"
155+
)
156+
157+
if skipped_dependencies:
158+
print(
159+
f"Info: Skipping {len(skipped_dependencies)} dependencies that are not present or not parseable in '{repo_branch}'"
160+
)
161+
for skipped in skipped_dependencies:
162+
print(f" - {skipped}")
163+
164+
return available_dependencies, skipped_dependencies
165+
166+
106167
def query_ghad(
107168
dependencies: dict[str, Dependency], gh_token: str, repo_path: Path
108169
) -> list[Vulnerability]:
@@ -237,7 +298,10 @@ def main() -> int:
237298
parser.add_argument(
238299
"node_repo_branch",
239300
metavar="NODE_REPO_BRANCH",
240-
help=f"the current branch of the Node repository (supports {supported_branches})",
301+
help=(
302+
"the current branch of the Node/N|Solid repository; known branches use "
303+
f"curated dependency lists, other branches are scanned by probing available dependencies ({supported_branches})"
304+
),
241305
)
242306
parser.add_argument(
243307
"--gh-token",
@@ -281,10 +345,6 @@ def main() -> int:
281345
raise RuntimeError(
282346
"Invalid argument: '{repo_path}' is not a valid Node git repository"
283347
)
284-
if repo_branch not in dependencies_per_branch:
285-
raise RuntimeError(
286-
f"Invalid argument: '{repo_branch}' is not a supported branch. Please use one of: {supported_branches}"
287-
)
288348
if gh_token is None:
289349
print(
290350
"Warning: GitHub authentication token not provided, skipping GitHub Advisory Database queries",
@@ -296,15 +356,17 @@ def main() -> int:
296356
file=sys.stderr,
297357
)
298358

299-
dependencies = {
300-
name: dep
301-
for name, dep in dependencies_info.items()
302-
if name in dependencies_per_branch[repo_branch]
303-
}
359+
dependencies, skipped_dependencies = resolve_dependencies(repo_path, repo_branch)
304360

305361
# Track whether every vulnerability source completed successfully. A partial scan must not
306362
# cause the reconciler to close issues for vulns that simply weren't queried this run.
307363
scan_complete = True
364+
if repo_branch in dependencies_per_branch and skipped_dependencies:
365+
scan_complete = False
366+
print(
367+
f"Warning: {len(skipped_dependencies)} curated dependencies could not be resolved for '{repo_branch}'",
368+
file=sys.stderr,
369+
)
308370

309371
ghad_vulnerabilities: list[Vulnerability] = []
310372
if gh_token is not None:
@@ -336,7 +398,7 @@ def main() -> int:
336398

337399
from npm_audit import NPMAuditChecker
338400
print("Running npm package vulnerability audit...", file=sys.stderr)
339-
npm_checker = NPMAuditChecker(repo_path, npm_timeout)
401+
npm_checker = NPMAuditChecker(repo_path, npm_timeout, gh_token=gh_token)
340402
npm_vulnerabilities = npm_checker.check_npm_vulnerabilities(Vulnerability)
341403
if npm_checker.failed_packages:
342404
scan_complete = False

0 commit comments

Comments
 (0)