Skip to content

Commit b33bd6c

Browse files
committed
dev dep only
1 parent 9fa6486 commit b33bd6c

2 files changed

Lines changed: 159 additions & 52 deletions

File tree

dep_checker/npm_audit.py

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""NPM package vulnerability checker using npm audit
22
33
This module handles npm package vulnerability scanning by:
4-
1. Finding all package.json files in the repository
5-
2. Running npm install --production in each directory
6-
3. Running npm audit --json to get vulnerability data
4+
1. Finding dep-root package.json files in the repository
5+
2. Reusing package-lock.json when present, otherwise installing non-dev dependencies
6+
3. Running npm audit against the prod dependency tree
77
4. Parsing results into Vulnerability objects
88
"""
99

@@ -55,7 +55,7 @@ def __init__(self, repo_path: Path, timeout: int = 300):
5555
self.failed_packages: List[str] = []
5656

5757
def find_package_json_files(self) -> List[Path]:
58-
"""Find all package.json files in the deps/ folder only, excluding specified folders"""
58+
"""Find dep-root package.json files in deps/, excluding nested package manifests."""
5959
package_json_files = []
6060
excluded_count = 0
6161

@@ -86,8 +86,17 @@ def find_package_json_files(self) -> List[Path]:
8686
logger.debug(f"Excluding {package_json} (matches exclusion: {exclude_path})")
8787
break
8888

89-
if not is_excluded:
90-
package_json_files.append(package_json)
89+
if is_excluded:
90+
continue
91+
92+
relative_to_deps = package_json.relative_to(deps_path)
93+
dep_root = deps_path / relative_to_deps.parts[0] / "package.json"
94+
if package_json != dep_root:
95+
# ponytail: only audit dep roots; add a nested allowlist if a shipped package ever lives deeper.
96+
logger.debug(f"Skipping nested package.json {package_json}")
97+
continue
98+
99+
package_json_files.append(package_json)
91100

92101
logger.info(f"Found {len(package_json_files)} package.json files in deps/ folder")
93102
if excluded_count > 0:
@@ -98,8 +107,12 @@ def find_package_json_files(self) -> List[Path]:
98107
logger.error(f"Error finding package.json files in deps/ folder: {e}")
99108
return []
100109

110+
def has_package_lock(self, package_dir: Path) -> bool:
111+
return (package_dir / "package-lock.json").exists()
112+
113+
101114
def run_npm_install(self, package_dir: Path) -> bool:
102-
"""Install production dependencies needed for an audit without lifecycle scripts."""
115+
"""Install non-dev dependencies needed for an audit when no lockfile is present."""
103116
try:
104117
logger.info(f"Running npm install in {package_dir}")
105118
# Check if npm is available
@@ -119,7 +132,7 @@ def run_npm_install(self, package_dir: Path) -> bool:
119132
# construct npm's dependency tree for an audit.
120133
result = subprocess.run(
121134
[
122-
"npm", "install", "--production", "--ignore-scripts",
135+
"npm", "install", "--omit=dev", "--ignore-scripts",
123136
"--no-audit", "--no-fund", "--silent",
124137
],
125138
cwd=package_dir,
@@ -145,12 +158,18 @@ def run_npm_install(self, package_dir: Path) -> bool:
145158
logger.error(f"Error running npm install in {package_dir}: {e}")
146159
return False
147160

148-
def run_npm_audit(self, package_dir: Path) -> Optional[Dict]:
149-
"""Run npm audit --json in the given directory"""
161+
def run_npm_audit(
162+
self, package_dir: Path, package_lock_only: bool = False
163+
) -> Optional[Dict]:
164+
"""Run npm audit against the production dependency tree in the given directory."""
150165
try:
151166
logger.info(f"Running npm audit in {package_dir}")
167+
command = ["npm", "audit", "--omit=dev"]
168+
if package_lock_only:
169+
command.append("--package-lock-only")
170+
command.append("--json")
152171
result = subprocess.run(
153-
["npm", "audit", "--json"],
172+
command,
154173
cwd=package_dir,
155174
capture_output=True,
156175
text=True,
@@ -347,14 +366,14 @@ def check_npm_vulnerabilities(self, vulnerability_class) -> List:
347366
logger.info(f"Processing {package_json}")
348367

349368
try:
350-
# Run npm install
351-
if not self.run_npm_install(package_dir):
369+
package_lock_only = self.has_package_lock(package_dir)
370+
371+
if not package_lock_only and not self.run_npm_install(package_dir):
352372
logger.warning(f"Skipping npm audit for {package_dir} due to install failure")
353373
self.failed_packages.append(f"{package_dir}: npm install failed")
354374
continue
355-
356-
# Run npm audit
357-
audit_data = self.run_npm_audit(package_dir)
375+
376+
audit_data = self.run_npm_audit(package_dir, package_lock_only=package_lock_only)
358377
if audit_data is None:
359378
logger.warning(f"Skipping vulnerability parsing for {package_dir} due to audit failure")
360379
self.failed_packages.append(f"{package_dir}: npm audit failed")

dep_checker/test_npm_audit.py

Lines changed: 124 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,149 @@
11
#!/usr/bin/env python3
22
"""Simple test script to verify npm audit functionality"""
33

4-
import tempfile
54
import json
5+
import tempfile
66
from pathlib import Path
7+
8+
import npm_audit
79
from npm_audit import NPMAuditChecker
8-
from main import Vulnerability
9-
10-
def create_test_package_json(temp_dir: Path) -> Path:
11-
"""Create a test package.json with known vulnerable packages"""
12-
package_json_content = {
13-
"name": "test-package",
14-
"version": "1.0.0",
15-
"dependencies": {
16-
# Using an older version that might have known vulnerabilities
17-
"lodash": "4.17.0"
18-
}
19-
}
20-
21-
package_json_path = temp_dir / "package.json"
22-
with open(package_json_path, 'w') as f:
23-
json.dump(package_json_content, f, indent=2)
24-
25-
return package_json_path
26-
27-
def test_npm_audit_basic():
10+
11+
12+
class Vulnerability:
13+
def __init__(self, **kwargs):
14+
self.__dict__.update(kwargs)
15+
16+
17+
def write_package_json(path: Path, content: dict) -> Path:
18+
path.parent.mkdir(parents=True, exist_ok=True)
19+
path.write_text(json.dumps(content, indent=2))
20+
return path
21+
22+
23+
def test_find_package_json_files_skips_nested_manifests() -> None:
24+
with tempfile.TemporaryDirectory() as temp_dir:
25+
temp_path = Path(temp_dir)
26+
root_package = write_package_json(
27+
temp_path / "deps" / "pkg" / "package.json",
28+
{"name": "pkg", "version": "1.0.0", "dependencies": {"lodash": "4.17.0"}},
29+
)
30+
write_package_json(
31+
temp_path / "deps" / "pkg" / "src" / "package.json",
32+
{"name": "pkg-src", "version": "1.0.0", "devDependencies": {"esbuild": "0.27.0"}},
33+
)
34+
35+
checker = NPMAuditChecker(temp_path, timeout=60)
36+
package_files = checker.find_package_json_files()
37+
38+
assert package_files == [root_package], package_files
39+
40+
41+
def test_npm_commands_omit_dev() -> None:
42+
calls = []
43+
44+
class Result:
45+
def __init__(self, returncode: int = 0, stdout: str = "", stderr: str = ""):
46+
self.returncode = returncode
47+
self.stdout = stdout
48+
self.stderr = stderr
49+
50+
def fake_run(cmd, **kwargs):
51+
calls.append(cmd)
52+
if cmd == ["npm", "--version"]:
53+
return Result(stdout="10.0.0\n")
54+
if cmd[:2] == ["npm", "install"]:
55+
return Result()
56+
if cmd[:2] == ["npm", "audit"]:
57+
return Result(stdout='{"vulnerabilities": {}}')
58+
raise AssertionError(cmd)
59+
60+
original_run = npm_audit.subprocess.run
61+
npm_audit.subprocess.run = fake_run
62+
try:
63+
checker = NPMAuditChecker(Path("/tmp"), timeout=60)
64+
assert checker.run_npm_install(Path("/tmp"))
65+
assert checker.run_npm_audit(Path("/tmp")) == {"vulnerabilities": {}}
66+
assert checker.run_npm_audit(Path("/tmp"), package_lock_only=True) == {"vulnerabilities": {}}
67+
finally:
68+
npm_audit.subprocess.run = original_run
69+
70+
assert ["npm", "install", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund", "--silent"] in calls
71+
assert ["npm", "audit", "--omit=dev", "--json"] in calls
72+
assert ["npm", "audit", "--omit=dev", "--package-lock-only", "--json"] in calls
73+
74+
75+
def test_lockfile_skips_install() -> None:
76+
calls = []
77+
78+
class Result:
79+
def __init__(self, returncode: int = 0, stdout: str = "", stderr: str = ""):
80+
self.returncode = returncode
81+
self.stdout = stdout
82+
self.stderr = stderr
83+
84+
def fake_run(cmd, **kwargs):
85+
calls.append(cmd)
86+
if cmd[:2] == ["npm", "audit"]:
87+
return Result(stdout='{"vulnerabilities": {}}')
88+
if cmd == ["npm", "--version"]:
89+
raise AssertionError("install should be skipped when package-lock.json exists")
90+
if cmd[:2] == ["npm", "install"]:
91+
raise AssertionError("install should be skipped when package-lock.json exists")
92+
raise AssertionError(cmd)
93+
94+
original_run = npm_audit.subprocess.run
95+
npm_audit.subprocess.run = fake_run
96+
try:
97+
with tempfile.TemporaryDirectory() as temp_dir:
98+
package_dir = Path(temp_dir) / "deps" / "pkg"
99+
write_package_json(
100+
package_dir / "package.json",
101+
{"name": "pkg", "version": "1.0.0", "dependencies": {"lodash": "4.17.0"}},
102+
)
103+
write_package_json(
104+
package_dir / "package-lock.json",
105+
{"name": "pkg", "lockfileVersion": 3},
106+
)
107+
108+
checker = NPMAuditChecker(Path(temp_dir), timeout=60)
109+
vulnerabilities = checker.check_npm_vulnerabilities(Vulnerability)
110+
assert vulnerabilities == [], vulnerabilities
111+
finally:
112+
npm_audit.subprocess.run = original_run
113+
114+
assert ["npm", "audit", "--omit=dev", "--package-lock-only", "--json"] in calls
115+
116+
117+
def test_npm_audit_basic() -> None:
28118
"""Test basic npm audit functionality"""
29119
print("Testing npm audit functionality...")
30-
120+
test_find_package_json_files_skips_nested_manifests()
121+
test_npm_commands_omit_dev()
122+
test_lockfile_skips_install()
123+
31124
with tempfile.TemporaryDirectory() as temp_dir:
32125
temp_path = Path(temp_dir)
33-
34-
# Create test package.json
35-
package_json = create_test_package_json(temp_path)
126+
package_json = write_package_json(
127+
temp_path / "deps" / "pkg" / "package.json",
128+
{"name": "pkg", "version": "1.0.0", "dependencies": {"lodash": "4.17.0"}},
129+
)
36130
print(f"Created test package.json at: {package_json}")
37-
38-
# Test NPM audit checker
131+
39132
checker = NPMAuditChecker(temp_path, timeout=60)
40-
41-
# Test finding package.json files
42133
package_files = checker.find_package_json_files()
43134
print(f"Found {len(package_files)} package.json files")
44-
assert len(package_files) == 1, "Should find exactly one package.json file"
45-
46-
# Test npm audit (this will only work if npm is available)
135+
assert package_files == [package_json], package_files
136+
47137
try:
48138
vulnerabilities = checker.check_npm_vulnerabilities(Vulnerability)
49139
print(f"Found {len(vulnerabilities)} vulnerabilities")
50-
51-
# Print vulnerability details
52140
for vuln in vulnerabilities:
53141
print(f"- {vuln.dependency} ({vuln.version}): {vuln.id} - {vuln.severity}")
54-
55142
except Exception as e:
56143
print(f"npm audit test failed (this is expected if npm is not available): {e}")
57-
144+
58145
print("Basic npm audit test completed!")
59146

147+
60148
if __name__ == "__main__":
61149
test_npm_audit_basic()

0 commit comments

Comments
 (0)