security-use 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- security_use/__init__.py +15 -0
- security_use/cli.py +348 -0
- security_use/dependency_scanner.py +199 -0
- security_use/fixers/__init__.py +6 -0
- security_use/fixers/dependency_fixer.py +196 -0
- security_use/fixers/iac_fixer.py +191 -0
- security_use/iac/__init__.py +9 -0
- security_use/iac/base.py +69 -0
- security_use/iac/cloudformation.py +207 -0
- security_use/iac/rules/__init__.py +29 -0
- security_use/iac/rules/aws.py +338 -0
- security_use/iac/rules/base.py +96 -0
- security_use/iac/rules/registry.py +115 -0
- security_use/iac/terraform.py +177 -0
- security_use/iac_scanner.py +215 -0
- security_use/models.py +139 -0
- security_use/osv_client.py +386 -0
- security_use/parsers/__init__.py +16 -0
- security_use/parsers/base.py +43 -0
- security_use/parsers/pipfile.py +133 -0
- security_use/parsers/poetry_lock.py +42 -0
- security_use/parsers/pyproject.py +178 -0
- security_use/parsers/requirements.py +86 -0
- security_use/py.typed +0 -0
- security_use/reporter.py +368 -0
- security_use/scanner.py +74 -0
- security_use-0.1.1.dist-info/METADATA +92 -0
- security_use-0.1.1.dist-info/RECORD +30 -0
- security_use-0.1.1.dist-info/WHEEL +4 -0
- security_use-0.1.1.dist-info/entry_points.txt +2 -0
security_use/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""security-use - Security scanning tool for dependencies and Infrastructure as Code."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.1"
|
|
4
|
+
|
|
5
|
+
from security_use.scanner import scan_dependencies, scan_iac
|
|
6
|
+
from security_use.models import Vulnerability, IaCFinding, ScanResult
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"__version__",
|
|
10
|
+
"scan_dependencies",
|
|
11
|
+
"scan_iac",
|
|
12
|
+
"Vulnerability",
|
|
13
|
+
"IaCFinding",
|
|
14
|
+
"ScanResult",
|
|
15
|
+
]
|
security_use/cli.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Command-line interface for security-use."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from security_use import __version__
|
|
11
|
+
from security_use.models import Severity, ScanResult
|
|
12
|
+
from security_use.reporter import create_reporter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_severity_threshold(severity: str) -> Severity:
|
|
19
|
+
"""Convert severity string to Severity enum."""
|
|
20
|
+
mapping = {
|
|
21
|
+
"critical": Severity.CRITICAL,
|
|
22
|
+
"high": Severity.HIGH,
|
|
23
|
+
"medium": Severity.MEDIUM,
|
|
24
|
+
"low": Severity.LOW,
|
|
25
|
+
}
|
|
26
|
+
return mapping.get(severity.lower(), Severity.LOW)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _filter_by_severity(result: ScanResult, threshold: Severity) -> ScanResult:
|
|
30
|
+
"""Filter results to only include issues at or above severity threshold."""
|
|
31
|
+
severity_order = {
|
|
32
|
+
Severity.CRITICAL: 0,
|
|
33
|
+
Severity.HIGH: 1,
|
|
34
|
+
Severity.MEDIUM: 2,
|
|
35
|
+
Severity.LOW: 3,
|
|
36
|
+
Severity.UNKNOWN: 4,
|
|
37
|
+
}
|
|
38
|
+
threshold_order = severity_order[threshold]
|
|
39
|
+
|
|
40
|
+
filtered = ScanResult(
|
|
41
|
+
scanned_files=result.scanned_files,
|
|
42
|
+
errors=result.errors,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
filtered.vulnerabilities = [
|
|
46
|
+
v for v in result.vulnerabilities
|
|
47
|
+
if severity_order.get(v.severity, 4) <= threshold_order
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
filtered.iac_findings = [
|
|
51
|
+
f for f in result.iac_findings
|
|
52
|
+
if severity_order.get(f.severity, 4) <= threshold_order
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
return filtered
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _output_result(
|
|
59
|
+
result: ScanResult,
|
|
60
|
+
format: str,
|
|
61
|
+
output: Optional[str],
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Output scan results in the specified format."""
|
|
64
|
+
reporter = create_reporter(format)
|
|
65
|
+
report = reporter.generate(result)
|
|
66
|
+
|
|
67
|
+
if output:
|
|
68
|
+
Path(output).write_text(report, encoding="utf-8")
|
|
69
|
+
console.print(f"[green]Report written to {output}[/green]")
|
|
70
|
+
else:
|
|
71
|
+
if format == "json" or format == "sarif":
|
|
72
|
+
click.echo(report)
|
|
73
|
+
else:
|
|
74
|
+
# Table format already outputs via rich
|
|
75
|
+
console.print(report)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@click.group()
|
|
79
|
+
@click.version_option(version=__version__, prog_name="security-use")
|
|
80
|
+
def main() -> None:
|
|
81
|
+
"""security-use - Security scanning tool for dependencies and IaC."""
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@main.group()
|
|
86
|
+
def scan() -> None:
|
|
87
|
+
"""Scan for security vulnerabilities."""
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@scan.command("deps")
|
|
92
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
93
|
+
@click.option(
|
|
94
|
+
"--format", "-f",
|
|
95
|
+
type=click.Choice(["json", "table", "sarif"]),
|
|
96
|
+
default="table",
|
|
97
|
+
help="Output format",
|
|
98
|
+
)
|
|
99
|
+
@click.option(
|
|
100
|
+
"--severity", "-s",
|
|
101
|
+
type=click.Choice(["critical", "high", "medium", "low"]),
|
|
102
|
+
default="low",
|
|
103
|
+
help="Minimum severity to report",
|
|
104
|
+
)
|
|
105
|
+
@click.option(
|
|
106
|
+
"--output", "-o",
|
|
107
|
+
type=click.Path(),
|
|
108
|
+
help="Write output to file",
|
|
109
|
+
)
|
|
110
|
+
def scan_deps(path: str, format: str, severity: str, output: Optional[str]) -> None:
|
|
111
|
+
"""Scan dependencies for known vulnerabilities.
|
|
112
|
+
|
|
113
|
+
PATH is the file or directory to scan (default: current directory).
|
|
114
|
+
"""
|
|
115
|
+
from security_use.dependency_scanner import DependencyScanner
|
|
116
|
+
|
|
117
|
+
is_machine_format = format in ("json", "sarif")
|
|
118
|
+
|
|
119
|
+
if not is_machine_format:
|
|
120
|
+
console.print(f"[blue]Scanning dependencies in {path}...[/blue]")
|
|
121
|
+
|
|
122
|
+
scanner = DependencyScanner()
|
|
123
|
+
result = scanner.scan_path(Path(path))
|
|
124
|
+
|
|
125
|
+
# Filter by severity
|
|
126
|
+
threshold = _get_severity_threshold(severity)
|
|
127
|
+
result = _filter_by_severity(result, threshold)
|
|
128
|
+
|
|
129
|
+
# Output results
|
|
130
|
+
_output_result(result, format, output)
|
|
131
|
+
|
|
132
|
+
# Exit with error code if vulnerabilities found
|
|
133
|
+
if result.vulnerabilities:
|
|
134
|
+
if not is_machine_format:
|
|
135
|
+
console.print(
|
|
136
|
+
f"\n[red]Found {len(result.vulnerabilities)} vulnerability(ies)[/red]"
|
|
137
|
+
)
|
|
138
|
+
sys.exit(1)
|
|
139
|
+
else:
|
|
140
|
+
if not is_machine_format:
|
|
141
|
+
console.print("\n[green]No vulnerabilities found[/green]")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@scan.command("iac")
|
|
145
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
146
|
+
@click.option(
|
|
147
|
+
"--format", "-f",
|
|
148
|
+
type=click.Choice(["json", "table", "sarif"]),
|
|
149
|
+
default="table",
|
|
150
|
+
help="Output format",
|
|
151
|
+
)
|
|
152
|
+
@click.option(
|
|
153
|
+
"--severity", "-s",
|
|
154
|
+
type=click.Choice(["critical", "high", "medium", "low"]),
|
|
155
|
+
default="low",
|
|
156
|
+
help="Minimum severity to report",
|
|
157
|
+
)
|
|
158
|
+
@click.option(
|
|
159
|
+
"--output", "-o",
|
|
160
|
+
type=click.Path(),
|
|
161
|
+
help="Write output to file",
|
|
162
|
+
)
|
|
163
|
+
def scan_iac(path: str, format: str, severity: str, output: Optional[str]) -> None:
|
|
164
|
+
"""Scan Infrastructure as Code for security misconfigurations.
|
|
165
|
+
|
|
166
|
+
PATH is the file or directory to scan (default: current directory).
|
|
167
|
+
"""
|
|
168
|
+
from security_use.iac_scanner import IaCScanner
|
|
169
|
+
|
|
170
|
+
is_machine_format = format in ("json", "sarif")
|
|
171
|
+
|
|
172
|
+
if not is_machine_format:
|
|
173
|
+
console.print(f"[blue]Scanning IaC files in {path}...[/blue]")
|
|
174
|
+
|
|
175
|
+
scanner = IaCScanner()
|
|
176
|
+
result = scanner.scan_path(Path(path))
|
|
177
|
+
|
|
178
|
+
# Filter by severity
|
|
179
|
+
threshold = _get_severity_threshold(severity)
|
|
180
|
+
result = _filter_by_severity(result, threshold)
|
|
181
|
+
|
|
182
|
+
# Output results
|
|
183
|
+
_output_result(result, format, output)
|
|
184
|
+
|
|
185
|
+
# Exit with error code if findings found
|
|
186
|
+
if result.iac_findings:
|
|
187
|
+
if not is_machine_format:
|
|
188
|
+
console.print(
|
|
189
|
+
f"\n[red]Found {len(result.iac_findings)} security issue(s)[/red]"
|
|
190
|
+
)
|
|
191
|
+
sys.exit(1)
|
|
192
|
+
else:
|
|
193
|
+
if not is_machine_format:
|
|
194
|
+
console.print("\n[green]No security issues found[/green]")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@scan.command("all")
|
|
198
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
199
|
+
@click.option(
|
|
200
|
+
"--format", "-f",
|
|
201
|
+
type=click.Choice(["json", "table", "sarif"]),
|
|
202
|
+
default="table",
|
|
203
|
+
help="Output format",
|
|
204
|
+
)
|
|
205
|
+
@click.option(
|
|
206
|
+
"--severity", "-s",
|
|
207
|
+
type=click.Choice(["critical", "high", "medium", "low"]),
|
|
208
|
+
default="low",
|
|
209
|
+
help="Minimum severity to report",
|
|
210
|
+
)
|
|
211
|
+
@click.option(
|
|
212
|
+
"--output", "-o",
|
|
213
|
+
type=click.Path(),
|
|
214
|
+
help="Write output to file",
|
|
215
|
+
)
|
|
216
|
+
def scan_all(path: str, format: str, severity: str, output: Optional[str]) -> None:
|
|
217
|
+
"""Scan both dependencies and IaC for security issues.
|
|
218
|
+
|
|
219
|
+
PATH is the file or directory to scan (default: current directory).
|
|
220
|
+
"""
|
|
221
|
+
from security_use.dependency_scanner import DependencyScanner
|
|
222
|
+
from security_use.iac_scanner import IaCScanner
|
|
223
|
+
|
|
224
|
+
is_machine_format = format in ("json", "sarif")
|
|
225
|
+
|
|
226
|
+
if not is_machine_format:
|
|
227
|
+
console.print(f"[blue]Scanning {path} for all security issues...[/blue]")
|
|
228
|
+
|
|
229
|
+
# Combined result
|
|
230
|
+
result = ScanResult()
|
|
231
|
+
|
|
232
|
+
# Scan dependencies
|
|
233
|
+
dep_scanner = DependencyScanner()
|
|
234
|
+
dep_result = dep_scanner.scan_path(Path(path))
|
|
235
|
+
result.vulnerabilities = dep_result.vulnerabilities
|
|
236
|
+
result.scanned_files.extend(dep_result.scanned_files)
|
|
237
|
+
result.errors.extend(dep_result.errors)
|
|
238
|
+
|
|
239
|
+
# Scan IaC
|
|
240
|
+
iac_scanner = IaCScanner()
|
|
241
|
+
iac_result = iac_scanner.scan_path(Path(path))
|
|
242
|
+
result.iac_findings = iac_result.iac_findings
|
|
243
|
+
result.scanned_files.extend(iac_result.scanned_files)
|
|
244
|
+
result.errors.extend(iac_result.errors)
|
|
245
|
+
|
|
246
|
+
# Filter by severity
|
|
247
|
+
threshold = _get_severity_threshold(severity)
|
|
248
|
+
result = _filter_by_severity(result, threshold)
|
|
249
|
+
|
|
250
|
+
# Output results
|
|
251
|
+
_output_result(result, format, output)
|
|
252
|
+
|
|
253
|
+
# Exit with error code if issues found
|
|
254
|
+
if result.total_issues > 0:
|
|
255
|
+
if not is_machine_format:
|
|
256
|
+
console.print(f"\n[red]Found {result.total_issues} security issue(s)[/red]")
|
|
257
|
+
sys.exit(1)
|
|
258
|
+
else:
|
|
259
|
+
if not is_machine_format:
|
|
260
|
+
console.print("\n[green]No security issues found[/green]")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@main.command()
|
|
264
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
265
|
+
@click.option(
|
|
266
|
+
"--dry-run",
|
|
267
|
+
is_flag=True,
|
|
268
|
+
help="Show what would be fixed without making changes",
|
|
269
|
+
)
|
|
270
|
+
def fix(path: str, dry_run: bool) -> None:
|
|
271
|
+
"""Auto-fix dependency vulnerabilities by updating versions.
|
|
272
|
+
|
|
273
|
+
PATH is the file or directory to scan and fix (default: current directory).
|
|
274
|
+
"""
|
|
275
|
+
from security_use.dependency_scanner import DependencyScanner
|
|
276
|
+
|
|
277
|
+
console.print(f"[blue]Scanning dependencies in {path}...[/blue]")
|
|
278
|
+
|
|
279
|
+
scanner = DependencyScanner()
|
|
280
|
+
result = scanner.scan_path(Path(path))
|
|
281
|
+
|
|
282
|
+
if not result.vulnerabilities:
|
|
283
|
+
console.print("[green]No vulnerabilities found - nothing to fix[/green]")
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
# Group vulnerabilities by package
|
|
287
|
+
package_fixes: dict[str, tuple[str, str]] = {}
|
|
288
|
+
for vuln in result.vulnerabilities:
|
|
289
|
+
if vuln.fixed_version and vuln.package not in package_fixes:
|
|
290
|
+
package_fixes[vuln.package] = (vuln.installed_version, vuln.fixed_version)
|
|
291
|
+
|
|
292
|
+
if not package_fixes:
|
|
293
|
+
console.print("[yellow]No automatic fixes available for found vulnerabilities[/yellow]")
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
console.print(f"\n[bold]Found {len(package_fixes)} package(s) to update:[/bold]\n")
|
|
297
|
+
|
|
298
|
+
for package, (current, fixed) in package_fixes.items():
|
|
299
|
+
console.print(f" • {package}: {current} → {fixed}")
|
|
300
|
+
|
|
301
|
+
if dry_run:
|
|
302
|
+
console.print("\n[yellow]Dry run - no changes made[/yellow]")
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
# Apply fixes
|
|
306
|
+
console.print("\n[blue]Applying fixes...[/blue]")
|
|
307
|
+
|
|
308
|
+
for file_path in result.scanned_files:
|
|
309
|
+
path_obj = Path(file_path)
|
|
310
|
+
if path_obj.suffix == ".txt" or path_obj.name == "requirements.txt":
|
|
311
|
+
_fix_requirements_file(path_obj, package_fixes)
|
|
312
|
+
|
|
313
|
+
console.print("[green]Fixes applied successfully[/green]")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _fix_requirements_file(
|
|
317
|
+
file_path: Path,
|
|
318
|
+
fixes: dict[str, tuple[str, str]],
|
|
319
|
+
) -> None:
|
|
320
|
+
"""Apply fixes to a requirements.txt file."""
|
|
321
|
+
import re
|
|
322
|
+
|
|
323
|
+
content = file_path.read_text(encoding="utf-8")
|
|
324
|
+
lines = content.splitlines()
|
|
325
|
+
modified = False
|
|
326
|
+
|
|
327
|
+
for i, line in enumerate(lines):
|
|
328
|
+
for package, (current, fixed) in fixes.items():
|
|
329
|
+
# Match package==version pattern
|
|
330
|
+
pattern = rf'^{re.escape(package)}==({re.escape(current)})'
|
|
331
|
+
match = re.match(pattern, line, re.IGNORECASE)
|
|
332
|
+
if match:
|
|
333
|
+
lines[i] = f"{package}=={fixed}"
|
|
334
|
+
modified = True
|
|
335
|
+
console.print(f" [green]Updated {package} in {file_path}[/green]")
|
|
336
|
+
|
|
337
|
+
if modified:
|
|
338
|
+
file_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@main.command()
|
|
342
|
+
def version() -> None:
|
|
343
|
+
"""Show version information."""
|
|
344
|
+
click.echo(f"security-use version {__version__}")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
if __name__ == "__main__":
|
|
348
|
+
main()
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Dependency scanner for detecting vulnerable packages."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from security_use.models import ScanResult, Vulnerability
|
|
7
|
+
from security_use.parsers import (
|
|
8
|
+
Dependency,
|
|
9
|
+
DependencyParser,
|
|
10
|
+
PipfileParser,
|
|
11
|
+
PoetryLockParser,
|
|
12
|
+
PyProjectParser,
|
|
13
|
+
RequirementsParser,
|
|
14
|
+
)
|
|
15
|
+
from security_use.parsers.pipfile import PipfileLockParser
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DependencyScanner:
|
|
19
|
+
"""Scanner for dependency files."""
|
|
20
|
+
|
|
21
|
+
PARSERS: list[type[DependencyParser]] = [
|
|
22
|
+
RequirementsParser,
|
|
23
|
+
PyProjectParser,
|
|
24
|
+
PipfileParser,
|
|
25
|
+
PipfileLockParser,
|
|
26
|
+
PoetryLockParser,
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
DEPENDENCY_FILES = [
|
|
30
|
+
"requirements.txt",
|
|
31
|
+
"requirements-dev.txt",
|
|
32
|
+
"requirements-test.txt",
|
|
33
|
+
"requirements.in",
|
|
34
|
+
"pyproject.toml",
|
|
35
|
+
"Pipfile",
|
|
36
|
+
"Pipfile.lock",
|
|
37
|
+
"poetry.lock",
|
|
38
|
+
"setup.py",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
def __init__(self) -> None:
|
|
42
|
+
"""Initialize the dependency scanner."""
|
|
43
|
+
self._osv_client: Optional["OSVClient"] = None
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def osv_client(self) -> "OSVClient":
|
|
47
|
+
"""Lazy-load the OSV client."""
|
|
48
|
+
if self._osv_client is None:
|
|
49
|
+
from security_use.osv_client import OSVClient
|
|
50
|
+
self._osv_client = OSVClient()
|
|
51
|
+
return self._osv_client
|
|
52
|
+
|
|
53
|
+
def scan_path(self, path: Path) -> ScanResult:
|
|
54
|
+
"""Scan a path for dependency vulnerabilities.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
path: File or directory path to scan.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
ScanResult containing vulnerabilities found.
|
|
61
|
+
"""
|
|
62
|
+
result = ScanResult()
|
|
63
|
+
|
|
64
|
+
if path.is_file():
|
|
65
|
+
files = [path]
|
|
66
|
+
else:
|
|
67
|
+
files = self._find_dependency_files(path)
|
|
68
|
+
|
|
69
|
+
for file_path in files:
|
|
70
|
+
try:
|
|
71
|
+
content = file_path.read_text(encoding="utf-8")
|
|
72
|
+
file_result = self.scan_content(content, file_path.name)
|
|
73
|
+
result.vulnerabilities.extend(file_result.vulnerabilities)
|
|
74
|
+
result.scanned_files.append(str(file_path))
|
|
75
|
+
except Exception as e:
|
|
76
|
+
result.errors.append(f"Error scanning {file_path}: {e}")
|
|
77
|
+
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
def scan_content(self, content: str, file_type: str) -> ScanResult:
|
|
81
|
+
"""Scan dependency file content for vulnerabilities.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
content: The file content to scan.
|
|
85
|
+
file_type: The filename or type (e.g., 'requirements.txt').
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
ScanResult containing vulnerabilities found.
|
|
89
|
+
"""
|
|
90
|
+
result = ScanResult()
|
|
91
|
+
|
|
92
|
+
# Parse dependencies
|
|
93
|
+
dependencies = self.parse_dependencies(content, file_type)
|
|
94
|
+
if not dependencies:
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
# Check for vulnerabilities
|
|
98
|
+
vulnerabilities = self.check_vulnerabilities(dependencies)
|
|
99
|
+
result.vulnerabilities = vulnerabilities
|
|
100
|
+
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
def parse_dependencies(self, content: str, file_type: str) -> list[Dependency]:
|
|
104
|
+
"""Parse dependencies from file content.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
content: The file content.
|
|
108
|
+
file_type: The filename to determine parser.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
List of parsed dependencies.
|
|
112
|
+
"""
|
|
113
|
+
parser = self._get_parser(file_type)
|
|
114
|
+
if parser is None:
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
return parser.parse(content)
|
|
118
|
+
|
|
119
|
+
def check_vulnerabilities(
|
|
120
|
+
self, dependencies: list[Dependency]
|
|
121
|
+
) -> list[Vulnerability]:
|
|
122
|
+
"""Check dependencies against vulnerability databases.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
dependencies: List of dependencies to check.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
List of vulnerabilities found.
|
|
129
|
+
"""
|
|
130
|
+
vulnerabilities = []
|
|
131
|
+
|
|
132
|
+
# Build package list for batch query
|
|
133
|
+
packages = [
|
|
134
|
+
(dep.name, dep.version)
|
|
135
|
+
for dep in dependencies
|
|
136
|
+
if dep.version is not None
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
if not packages:
|
|
140
|
+
return vulnerabilities
|
|
141
|
+
|
|
142
|
+
# Query OSV for vulnerabilities
|
|
143
|
+
vuln_results = self.osv_client.query_batch(packages)
|
|
144
|
+
|
|
145
|
+
for dep in dependencies:
|
|
146
|
+
if dep.version is None:
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
key = (dep.normalized_name, dep.version)
|
|
150
|
+
if key in vuln_results:
|
|
151
|
+
vulnerabilities.extend(vuln_results[key])
|
|
152
|
+
|
|
153
|
+
return vulnerabilities
|
|
154
|
+
|
|
155
|
+
def _find_dependency_files(self, directory: Path) -> list[Path]:
|
|
156
|
+
"""Find all dependency files in a directory.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
directory: Directory to search.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
List of dependency file paths.
|
|
163
|
+
"""
|
|
164
|
+
files = []
|
|
165
|
+
|
|
166
|
+
for filename in self.DEPENDENCY_FILES:
|
|
167
|
+
# Check root directory
|
|
168
|
+
file_path = directory / filename
|
|
169
|
+
if file_path.exists():
|
|
170
|
+
files.append(file_path)
|
|
171
|
+
|
|
172
|
+
# Check subdirectories (one level deep)
|
|
173
|
+
for subdir in directory.iterdir():
|
|
174
|
+
if subdir.is_dir() and not subdir.name.startswith("."):
|
|
175
|
+
sub_file = subdir / filename
|
|
176
|
+
if sub_file.exists():
|
|
177
|
+
files.append(sub_file)
|
|
178
|
+
|
|
179
|
+
return files
|
|
180
|
+
|
|
181
|
+
def _get_parser(self, file_type: str) -> Optional[DependencyParser]:
|
|
182
|
+
"""Get the appropriate parser for a file type.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
file_type: The filename or file type.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Parser instance or None if unsupported.
|
|
189
|
+
"""
|
|
190
|
+
filename = file_type.lower()
|
|
191
|
+
|
|
192
|
+
for parser_class in self.PARSERS:
|
|
193
|
+
if any(
|
|
194
|
+
supported.lower() in filename
|
|
195
|
+
for supported in parser_class.supported_filenames()
|
|
196
|
+
):
|
|
197
|
+
return parser_class()
|
|
198
|
+
|
|
199
|
+
return None
|