security-use 0.1.1__py3-none-any.whl → 0.2.9__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 +9 -1
- security_use/auth/__init__.py +16 -0
- security_use/auth/client.py +223 -0
- security_use/auth/config.py +177 -0
- security_use/auth/oauth.py +317 -0
- security_use/cli.py +699 -34
- security_use/compliance/__init__.py +10 -0
- security_use/compliance/mapper.py +275 -0
- security_use/compliance/models.py +50 -0
- security_use/dependency_scanner.py +76 -30
- security_use/fixers/iac_fixer.py +173 -95
- security_use/iac/rules/azure.py +246 -0
- security_use/iac/rules/gcp.py +255 -0
- security_use/iac/rules/kubernetes.py +429 -0
- security_use/iac/rules/registry.py +56 -0
- security_use/parsers/__init__.py +18 -0
- security_use/parsers/base.py +2 -0
- security_use/parsers/composer.py +101 -0
- security_use/parsers/conda.py +97 -0
- security_use/parsers/dotnet.py +89 -0
- security_use/parsers/gradle.py +90 -0
- security_use/parsers/maven.py +108 -0
- security_use/parsers/npm.py +196 -0
- security_use/parsers/yarn.py +108 -0
- security_use/reporter.py +29 -1
- security_use/sbom/__init__.py +10 -0
- security_use/sbom/generator.py +340 -0
- security_use/sbom/models.py +40 -0
- security_use/scanner.py +15 -2
- security_use/sensor/__init__.py +125 -0
- security_use/sensor/alert_queue.py +207 -0
- security_use/sensor/config.py +217 -0
- security_use/sensor/dashboard_alerter.py +246 -0
- security_use/sensor/detector.py +415 -0
- security_use/sensor/endpoint_analyzer.py +339 -0
- security_use/sensor/middleware.py +521 -0
- security_use/sensor/models.py +140 -0
- security_use/sensor/webhook.py +227 -0
- security_use-0.2.9.dist-info/METADATA +531 -0
- security_use-0.2.9.dist-info/RECORD +60 -0
- security_use-0.2.9.dist-info/licenses/LICENSE +21 -0
- security_use-0.1.1.dist-info/METADATA +0 -92
- security_use-0.1.1.dist-info/RECORD +0 -30
- {security_use-0.1.1.dist-info → security_use-0.2.9.dist-info}/WHEEL +0 -0
- {security_use-0.1.1.dist-info → security_use-0.2.9.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Compliance framework mapping module."""
|
|
2
|
+
|
|
3
|
+
from security_use.compliance.mapper import ComplianceMapper
|
|
4
|
+
from security_use.compliance.models import ComplianceFramework, ComplianceMapping
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"ComplianceMapper",
|
|
8
|
+
"ComplianceFramework",
|
|
9
|
+
"ComplianceMapping",
|
|
10
|
+
]
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Compliance framework mapper for security rules."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from security_use.compliance.models import (
|
|
6
|
+
ComplianceControl,
|
|
7
|
+
ComplianceFinding,
|
|
8
|
+
ComplianceFramework,
|
|
9
|
+
ComplianceMapping,
|
|
10
|
+
)
|
|
11
|
+
from security_use.models import IaCFinding
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ComplianceMapper:
|
|
15
|
+
"""Maps security findings to compliance framework controls."""
|
|
16
|
+
|
|
17
|
+
# Mapping of rule IDs to compliance controls
|
|
18
|
+
MAPPINGS: dict[str, list[tuple[ComplianceFramework, str, str, str]]] = {
|
|
19
|
+
# AWS S3 Encryption
|
|
20
|
+
"CKV_AWS_19": [
|
|
21
|
+
(ComplianceFramework.SOC2, "CC6.1", "Encryption of Data at Rest",
|
|
22
|
+
"Logical and physical access controls"),
|
|
23
|
+
(ComplianceFramework.HIPAA, "164.312(a)(2)(iv)", "Encryption",
|
|
24
|
+
"Technical safeguards for data encryption"),
|
|
25
|
+
(ComplianceFramework.PCI_DSS, "3.4", "Render PAN unreadable",
|
|
26
|
+
"Protect stored cardholder data"),
|
|
27
|
+
(ComplianceFramework.NIST_800_53, "SC-28", "Protection of Information at Rest",
|
|
28
|
+
"Protect data at rest with encryption"),
|
|
29
|
+
(ComplianceFramework.CIS_AWS, "2.1.1", "Ensure S3 Bucket Encryption",
|
|
30
|
+
"S3 buckets should have encryption enabled"),
|
|
31
|
+
(ComplianceFramework.ISO_27001, "A.10.1.1", "Cryptographic Controls",
|
|
32
|
+
"Policy on use of cryptographic controls"),
|
|
33
|
+
],
|
|
34
|
+
# AWS S3 Public Access
|
|
35
|
+
"CKV_AWS_20": [
|
|
36
|
+
(ComplianceFramework.SOC2, "CC6.6", "Logical Access Controls",
|
|
37
|
+
"Restrict access to authorized users"),
|
|
38
|
+
(ComplianceFramework.HIPAA, "164.312(e)(1)", "Transmission Security",
|
|
39
|
+
"Technical safeguards for access control"),
|
|
40
|
+
(ComplianceFramework.PCI_DSS, "7.1", "Limit access to system components",
|
|
41
|
+
"Restrict access to need-to-know basis"),
|
|
42
|
+
(ComplianceFramework.NIST_800_53, "AC-3", "Access Enforcement",
|
|
43
|
+
"Enforce approved authorizations"),
|
|
44
|
+
(ComplianceFramework.CIS_AWS, "2.1.5", "Ensure S3 Block Public Access",
|
|
45
|
+
"Block public access to S3 buckets"),
|
|
46
|
+
],
|
|
47
|
+
# AWS Security Group
|
|
48
|
+
"CKV_AWS_23": [
|
|
49
|
+
(ComplianceFramework.SOC2, "CC6.6", "Logical Access Controls",
|
|
50
|
+
"Network access restrictions"),
|
|
51
|
+
(ComplianceFramework.PCI_DSS, "1.3", "Prohibit direct public access",
|
|
52
|
+
"Restrict inbound and outbound traffic"),
|
|
53
|
+
(ComplianceFramework.NIST_800_53, "SC-7", "Boundary Protection",
|
|
54
|
+
"Monitor and control communications at boundaries"),
|
|
55
|
+
(ComplianceFramework.CIS_AWS, "5.2", "Ensure VPC Security Groups",
|
|
56
|
+
"Restrict unrestricted ingress"),
|
|
57
|
+
],
|
|
58
|
+
# AWS RDS Encryption
|
|
59
|
+
"CKV_AWS_16": [
|
|
60
|
+
(ComplianceFramework.SOC2, "CC6.1", "Encryption of Data at Rest",
|
|
61
|
+
"Database encryption"),
|
|
62
|
+
(ComplianceFramework.HIPAA, "164.312(a)(2)(iv)", "Encryption",
|
|
63
|
+
"Encrypt PHI at rest"),
|
|
64
|
+
(ComplianceFramework.PCI_DSS, "3.4", "Render PAN unreadable",
|
|
65
|
+
"Encrypt stored cardholder data"),
|
|
66
|
+
(ComplianceFramework.NIST_800_53, "SC-28", "Protection of Information at Rest",
|
|
67
|
+
"Database encryption at rest"),
|
|
68
|
+
(ComplianceFramework.CIS_AWS, "2.3.1", "Ensure RDS Encryption",
|
|
69
|
+
"RDS instances should be encrypted"),
|
|
70
|
+
],
|
|
71
|
+
# AWS EBS Encryption
|
|
72
|
+
"CKV_AWS_3": [
|
|
73
|
+
(ComplianceFramework.SOC2, "CC6.1", "Encryption of Data at Rest",
|
|
74
|
+
"Volume encryption"),
|
|
75
|
+
(ComplianceFramework.HIPAA, "164.312(a)(2)(iv)", "Encryption",
|
|
76
|
+
"Encrypt PHI on storage volumes"),
|
|
77
|
+
(ComplianceFramework.PCI_DSS, "3.4", "Render PAN unreadable",
|
|
78
|
+
"Encrypt stored data on volumes"),
|
|
79
|
+
(ComplianceFramework.CIS_AWS, "2.2.1", "Ensure EBS Encryption",
|
|
80
|
+
"EBS volumes should be encrypted"),
|
|
81
|
+
],
|
|
82
|
+
# AWS IAM MFA
|
|
83
|
+
"CKV_AWS_14": [
|
|
84
|
+
(ComplianceFramework.SOC2, "CC6.1", "Authentication Controls",
|
|
85
|
+
"Multi-factor authentication"),
|
|
86
|
+
(ComplianceFramework.HIPAA, "164.312(d)", "Person or Entity Authentication",
|
|
87
|
+
"Verify user identity"),
|
|
88
|
+
(ComplianceFramework.PCI_DSS, "8.3", "Secure all administrative access",
|
|
89
|
+
"Use MFA for all access"),
|
|
90
|
+
(ComplianceFramework.NIST_800_53, "IA-2", "Identification and Authentication",
|
|
91
|
+
"Multi-factor authentication"),
|
|
92
|
+
(ComplianceFramework.CIS_AWS, "1.10", "Ensure MFA is enabled",
|
|
93
|
+
"Enable MFA for IAM users"),
|
|
94
|
+
],
|
|
95
|
+
# AWS CloudTrail
|
|
96
|
+
"CKV_AWS_35": [
|
|
97
|
+
(ComplianceFramework.SOC2, "CC7.2", "System Monitoring",
|
|
98
|
+
"Audit logging and monitoring"),
|
|
99
|
+
(ComplianceFramework.HIPAA, "164.312(b)", "Audit Controls",
|
|
100
|
+
"Record and examine activity"),
|
|
101
|
+
(ComplianceFramework.PCI_DSS, "10.1", "Implement audit trails",
|
|
102
|
+
"Link all access to individual users"),
|
|
103
|
+
(ComplianceFramework.NIST_800_53, "AU-2", "Audit Events",
|
|
104
|
+
"Generate audit records"),
|
|
105
|
+
(ComplianceFramework.CIS_AWS, "3.1", "Ensure CloudTrail is enabled",
|
|
106
|
+
"Enable CloudTrail in all regions"),
|
|
107
|
+
],
|
|
108
|
+
# AWS VPC Flow Logs
|
|
109
|
+
"CKV_AWS_12": [
|
|
110
|
+
(ComplianceFramework.SOC2, "CC7.2", "System Monitoring",
|
|
111
|
+
"Network traffic monitoring"),
|
|
112
|
+
(ComplianceFramework.PCI_DSS, "10.6", "Review logs",
|
|
113
|
+
"Monitor network traffic"),
|
|
114
|
+
(ComplianceFramework.NIST_800_53, "AU-12", "Audit Generation",
|
|
115
|
+
"Generate audit records for network traffic"),
|
|
116
|
+
(ComplianceFramework.CIS_AWS, "3.9", "Ensure VPC Flow Logs",
|
|
117
|
+
"Enable VPC Flow Logs"),
|
|
118
|
+
],
|
|
119
|
+
# Azure Storage Public Access
|
|
120
|
+
"CKV_AZURE_19": [
|
|
121
|
+
(ComplianceFramework.SOC2, "CC6.6", "Logical Access Controls",
|
|
122
|
+
"Storage access controls"),
|
|
123
|
+
(ComplianceFramework.CIS_AZURE, "3.7", "Ensure Storage Account Access",
|
|
124
|
+
"Disable public blob access"),
|
|
125
|
+
],
|
|
126
|
+
# Azure NSG
|
|
127
|
+
"CKV_AZURE_9": [
|
|
128
|
+
(ComplianceFramework.SOC2, "CC6.6", "Network Access Controls",
|
|
129
|
+
"NSG configuration"),
|
|
130
|
+
(ComplianceFramework.CIS_AZURE, "6.1", "Ensure NSG Rules",
|
|
131
|
+
"Restrict unrestricted access"),
|
|
132
|
+
],
|
|
133
|
+
# GCP Storage Public Access
|
|
134
|
+
"CKV_GCP_5": [
|
|
135
|
+
(ComplianceFramework.SOC2, "CC6.6", "Logical Access Controls",
|
|
136
|
+
"Cloud Storage access controls"),
|
|
137
|
+
(ComplianceFramework.CIS_GCP, "5.1", "Ensure Cloud Storage Bucket Access",
|
|
138
|
+
"Remove public access"),
|
|
139
|
+
],
|
|
140
|
+
# GCP Firewall
|
|
141
|
+
"CKV_GCP_2": [
|
|
142
|
+
(ComplianceFramework.SOC2, "CC6.6", "Network Access Controls",
|
|
143
|
+
"Firewall configuration"),
|
|
144
|
+
(ComplianceFramework.CIS_GCP, "3.6", "Ensure Firewall Rules",
|
|
145
|
+
"Restrict unrestricted access"),
|
|
146
|
+
],
|
|
147
|
+
# Kubernetes Privileged Container
|
|
148
|
+
"CKV_K8S_1": [
|
|
149
|
+
(ComplianceFramework.SOC2, "CC6.1", "Access Controls",
|
|
150
|
+
"Container privilege restrictions"),
|
|
151
|
+
(ComplianceFramework.CIS_K8S, "5.2.1", "Minimize privileged containers",
|
|
152
|
+
"Do not run privileged containers"),
|
|
153
|
+
],
|
|
154
|
+
# Kubernetes Run as Root
|
|
155
|
+
"CKV_K8S_6": [
|
|
156
|
+
(ComplianceFramework.SOC2, "CC6.1", "Access Controls",
|
|
157
|
+
"Container user restrictions"),
|
|
158
|
+
(ComplianceFramework.CIS_K8S, "5.2.6", "Minimize root containers",
|
|
159
|
+
"Do not run containers as root"),
|
|
160
|
+
],
|
|
161
|
+
# Kubernetes Resource Limits
|
|
162
|
+
"CKV_K8S_11": [
|
|
163
|
+
(ComplianceFramework.SOC2, "CC6.8", "System Operations",
|
|
164
|
+
"Resource management"),
|
|
165
|
+
(ComplianceFramework.CIS_K8S, "5.4.1", "Ensure resource limits",
|
|
166
|
+
"Set CPU and memory limits"),
|
|
167
|
+
],
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
def get_mapping(self, rule_id: str) -> ComplianceMapping:
|
|
171
|
+
"""Get compliance mapping for a rule.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
rule_id: The security rule ID.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
ComplianceMapping with associated controls.
|
|
178
|
+
"""
|
|
179
|
+
mapping = ComplianceMapping(rule_id=rule_id)
|
|
180
|
+
|
|
181
|
+
raw_mappings = self.MAPPINGS.get(rule_id, [])
|
|
182
|
+
for framework, control_id, title, description in raw_mappings:
|
|
183
|
+
control = ComplianceControl(
|
|
184
|
+
framework=framework,
|
|
185
|
+
control_id=control_id,
|
|
186
|
+
title=title,
|
|
187
|
+
description=description,
|
|
188
|
+
)
|
|
189
|
+
mapping.controls.append(control)
|
|
190
|
+
|
|
191
|
+
return mapping
|
|
192
|
+
|
|
193
|
+
def enrich_finding(self, finding: IaCFinding) -> ComplianceFinding:
|
|
194
|
+
"""Enrich an IaC finding with compliance information.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
finding: The original IaC finding.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
ComplianceFinding with compliance controls.
|
|
201
|
+
"""
|
|
202
|
+
mapping = self.get_mapping(finding.rule_id)
|
|
203
|
+
|
|
204
|
+
return ComplianceFinding(
|
|
205
|
+
rule_id=finding.rule_id,
|
|
206
|
+
title=finding.title,
|
|
207
|
+
severity=finding.severity.value,
|
|
208
|
+
file_path=finding.file_path,
|
|
209
|
+
line_number=finding.line_number,
|
|
210
|
+
controls=mapping.controls,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def get_findings_by_framework(
|
|
214
|
+
self,
|
|
215
|
+
findings: list[IaCFinding],
|
|
216
|
+
framework: ComplianceFramework,
|
|
217
|
+
) -> list[ComplianceFinding]:
|
|
218
|
+
"""Filter findings by compliance framework.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
findings: List of IaC findings.
|
|
222
|
+
framework: The compliance framework to filter by.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
List of ComplianceFindings relevant to the framework.
|
|
226
|
+
"""
|
|
227
|
+
result = []
|
|
228
|
+
|
|
229
|
+
for finding in findings:
|
|
230
|
+
enriched = self.enrich_finding(finding)
|
|
231
|
+
# Filter controls to only the requested framework
|
|
232
|
+
framework_controls = [
|
|
233
|
+
c for c in enriched.controls
|
|
234
|
+
if c.framework == framework
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
if framework_controls:
|
|
238
|
+
enriched.controls = framework_controls
|
|
239
|
+
result.append(enriched)
|
|
240
|
+
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
def get_supported_frameworks(self) -> list[ComplianceFramework]:
|
|
244
|
+
"""Get list of supported compliance frameworks.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
List of supported ComplianceFramework values.
|
|
248
|
+
"""
|
|
249
|
+
return list(ComplianceFramework)
|
|
250
|
+
|
|
251
|
+
def get_framework_summary(
|
|
252
|
+
self,
|
|
253
|
+
findings: list[IaCFinding],
|
|
254
|
+
framework: ComplianceFramework,
|
|
255
|
+
) -> dict[str, list[ComplianceFinding]]:
|
|
256
|
+
"""Group findings by control within a framework.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
findings: List of IaC findings.
|
|
260
|
+
framework: The compliance framework.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Dict mapping control_id to list of findings.
|
|
264
|
+
"""
|
|
265
|
+
by_control: dict[str, list[ComplianceFinding]] = {}
|
|
266
|
+
|
|
267
|
+
framework_findings = self.get_findings_by_framework(findings, framework)
|
|
268
|
+
|
|
269
|
+
for finding in framework_findings:
|
|
270
|
+
for control in finding.controls:
|
|
271
|
+
if control.control_id not in by_control:
|
|
272
|
+
by_control[control.control_id] = []
|
|
273
|
+
by_control[control.control_id].append(finding)
|
|
274
|
+
|
|
275
|
+
return by_control
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Data models for compliance mapping."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ComplianceFramework(Enum):
|
|
9
|
+
"""Supported compliance frameworks."""
|
|
10
|
+
|
|
11
|
+
SOC2 = "soc2"
|
|
12
|
+
HIPAA = "hipaa"
|
|
13
|
+
PCI_DSS = "pci-dss"
|
|
14
|
+
NIST_800_53 = "nist-800-53"
|
|
15
|
+
CIS_AWS = "cis-aws"
|
|
16
|
+
CIS_AZURE = "cis-azure"
|
|
17
|
+
CIS_GCP = "cis-gcp"
|
|
18
|
+
CIS_K8S = "cis-kubernetes"
|
|
19
|
+
ISO_27001 = "iso-27001"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ComplianceControl:
|
|
24
|
+
"""Represents a compliance control/requirement."""
|
|
25
|
+
|
|
26
|
+
framework: ComplianceFramework
|
|
27
|
+
control_id: str
|
|
28
|
+
title: str
|
|
29
|
+
description: str
|
|
30
|
+
category: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ComplianceMapping:
|
|
35
|
+
"""Maps a security rule to compliance controls."""
|
|
36
|
+
|
|
37
|
+
rule_id: str
|
|
38
|
+
controls: list[ComplianceControl] = field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class ComplianceFinding:
|
|
43
|
+
"""A finding with compliance context."""
|
|
44
|
+
|
|
45
|
+
rule_id: str
|
|
46
|
+
title: str
|
|
47
|
+
severity: str
|
|
48
|
+
file_path: str
|
|
49
|
+
line_number: int
|
|
50
|
+
controls: list[ComplianceControl] = field(default_factory=list)
|
|
@@ -7,10 +7,21 @@ from security_use.models import ScanResult, Vulnerability
|
|
|
7
7
|
from security_use.parsers import (
|
|
8
8
|
Dependency,
|
|
9
9
|
DependencyParser,
|
|
10
|
+
MavenParser,
|
|
11
|
+
NpmLockParser,
|
|
12
|
+
NpmParser,
|
|
10
13
|
PipfileParser,
|
|
11
14
|
PoetryLockParser,
|
|
12
15
|
PyProjectParser,
|
|
13
16
|
RequirementsParser,
|
|
17
|
+
GradleParser,
|
|
18
|
+
YarnLockParser,
|
|
19
|
+
PnpmLockParser,
|
|
20
|
+
CsprojParser,
|
|
21
|
+
PackagesConfigParser,
|
|
22
|
+
CondaEnvironmentParser,
|
|
23
|
+
ComposerParser,
|
|
24
|
+
ComposerLockParser,
|
|
14
25
|
)
|
|
15
26
|
from security_use.parsers.pipfile import PipfileLockParser
|
|
16
27
|
|
|
@@ -24,6 +35,17 @@ class DependencyScanner:
|
|
|
24
35
|
PipfileParser,
|
|
25
36
|
PipfileLockParser,
|
|
26
37
|
PoetryLockParser,
|
|
38
|
+
MavenParser,
|
|
39
|
+
NpmParser,
|
|
40
|
+
NpmLockParser,
|
|
41
|
+
GradleParser,
|
|
42
|
+
YarnLockParser,
|
|
43
|
+
PnpmLockParser,
|
|
44
|
+
CsprojParser,
|
|
45
|
+
PackagesConfigParser,
|
|
46
|
+
CondaEnvironmentParser,
|
|
47
|
+
ComposerParser,
|
|
48
|
+
ComposerLockParser,
|
|
27
49
|
]
|
|
28
50
|
|
|
29
51
|
DEPENDENCY_FILES = [
|
|
@@ -36,6 +58,18 @@ class DependencyScanner:
|
|
|
36
58
|
"Pipfile.lock",
|
|
37
59
|
"poetry.lock",
|
|
38
60
|
"setup.py",
|
|
61
|
+
"pom.xml",
|
|
62
|
+
"package.json",
|
|
63
|
+
"package-lock.json",
|
|
64
|
+
"build.gradle",
|
|
65
|
+
"build.gradle.kts",
|
|
66
|
+
"yarn.lock",
|
|
67
|
+
"pnpm-lock.yaml",
|
|
68
|
+
"packages.config",
|
|
69
|
+
"environment.yml",
|
|
70
|
+
"environment.yaml",
|
|
71
|
+
"composer.json",
|
|
72
|
+
"composer.lock",
|
|
39
73
|
]
|
|
40
74
|
|
|
41
75
|
def __init__(self) -> None:
|
|
@@ -129,53 +163,65 @@ class DependencyScanner:
|
|
|
129
163
|
"""
|
|
130
164
|
vulnerabilities = []
|
|
131
165
|
|
|
132
|
-
#
|
|
133
|
-
|
|
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
|
-
|
|
166
|
+
# Group dependencies by ecosystem
|
|
167
|
+
by_ecosystem: dict[str, list[Dependency]] = {}
|
|
145
168
|
for dep in dependencies:
|
|
146
169
|
if dep.version is None:
|
|
147
170
|
continue
|
|
171
|
+
ecosystem = getattr(dep, "ecosystem", "PyPI") or "PyPI"
|
|
172
|
+
if ecosystem not in by_ecosystem:
|
|
173
|
+
by_ecosystem[ecosystem] = []
|
|
174
|
+
by_ecosystem[ecosystem].append(dep)
|
|
148
175
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
176
|
+
# Query each ecosystem separately
|
|
177
|
+
for ecosystem, deps in by_ecosystem.items():
|
|
178
|
+
packages = [(dep.name, dep.version) for dep in deps]
|
|
179
|
+
|
|
180
|
+
if not packages:
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
# Query OSV for vulnerabilities
|
|
184
|
+
vuln_results = self.osv_client.query_batch(packages, ecosystem=ecosystem)
|
|
185
|
+
|
|
186
|
+
for dep in deps:
|
|
187
|
+
key = (dep.normalized_name, dep.version)
|
|
188
|
+
if key in vuln_results:
|
|
189
|
+
vulnerabilities.extend(vuln_results[key])
|
|
152
190
|
|
|
153
191
|
return vulnerabilities
|
|
154
192
|
|
|
155
|
-
def _find_dependency_files(self, directory: Path) -> list[Path]:
|
|
156
|
-
"""Find all dependency files in a directory.
|
|
193
|
+
def _find_dependency_files(self, directory: Path, max_depth: int = 4) -> list[Path]:
|
|
194
|
+
"""Find all dependency files in a directory recursively.
|
|
157
195
|
|
|
158
196
|
Args:
|
|
159
197
|
directory: Directory to search.
|
|
198
|
+
max_depth: Maximum depth to search (default: 4).
|
|
160
199
|
|
|
161
200
|
Returns:
|
|
162
201
|
List of dependency file paths.
|
|
163
202
|
"""
|
|
164
203
|
files = []
|
|
204
|
+
# Directories to skip (contain many nested dependencies we don't want to scan)
|
|
205
|
+
skip_dirs = {
|
|
206
|
+
"node_modules", ".git", ".venv", "venv", "__pycache__",
|
|
207
|
+
".tox", ".pytest_cache", "dist", "build", ".eggs",
|
|
208
|
+
"target", ".gradle", ".m2"
|
|
209
|
+
}
|
|
165
210
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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)
|
|
211
|
+
def search_dir(path: Path, depth: int) -> None:
|
|
212
|
+
if depth > max_depth:
|
|
213
|
+
return
|
|
178
214
|
|
|
215
|
+
try:
|
|
216
|
+
for item in path.iterdir():
|
|
217
|
+
if item.is_file() and item.name in self.DEPENDENCY_FILES:
|
|
218
|
+
files.append(item)
|
|
219
|
+
elif item.is_dir() and not item.name.startswith(".") and item.name not in skip_dirs:
|
|
220
|
+
search_dir(item, depth + 1)
|
|
221
|
+
except PermissionError:
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
search_dir(directory, 0)
|
|
179
225
|
return files
|
|
180
226
|
|
|
181
227
|
def _get_parser(self, file_type: str) -> Optional[DependencyParser]:
|