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
|
@@ -94,6 +94,7 @@ def get_registry() -> RuleRegistry:
|
|
|
94
94
|
|
|
95
95
|
def _register_default_rules(registry: RuleRegistry) -> None:
|
|
96
96
|
"""Register all default rules."""
|
|
97
|
+
# AWS Rules
|
|
97
98
|
from security_use.iac.rules.aws import (
|
|
98
99
|
S3BucketEncryptionRule,
|
|
99
100
|
S3BucketPublicAccessRule,
|
|
@@ -113,3 +114,58 @@ def _register_default_rules(registry: RuleRegistry) -> None:
|
|
|
113
114
|
registry.register_class(EBSEncryptionRule)
|
|
114
115
|
registry.register_class(CloudTrailEnabledRule)
|
|
115
116
|
registry.register_class(VPCFlowLogsRule)
|
|
117
|
+
|
|
118
|
+
# Azure Rules
|
|
119
|
+
from security_use.iac.rules.azure import (
|
|
120
|
+
AzureStoragePublicAccessRule,
|
|
121
|
+
AzureStorageEncryptionRule,
|
|
122
|
+
AzureNSGOpenIngressRule,
|
|
123
|
+
AzureSQLEncryptionRule,
|
|
124
|
+
AzureKeyVaultSoftDeleteRule,
|
|
125
|
+
AzureActivityLogRetentionRule,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
registry.register_class(AzureStoragePublicAccessRule)
|
|
129
|
+
registry.register_class(AzureStorageEncryptionRule)
|
|
130
|
+
registry.register_class(AzureNSGOpenIngressRule)
|
|
131
|
+
registry.register_class(AzureSQLEncryptionRule)
|
|
132
|
+
registry.register_class(AzureKeyVaultSoftDeleteRule)
|
|
133
|
+
registry.register_class(AzureActivityLogRetentionRule)
|
|
134
|
+
|
|
135
|
+
# GCP Rules
|
|
136
|
+
from security_use.iac.rules.gcp import (
|
|
137
|
+
GCSBucketPublicAccessRule,
|
|
138
|
+
GCSBucketEncryptionRule,
|
|
139
|
+
GCPFirewallOpenIngressRule,
|
|
140
|
+
GCPCloudSQLEncryptionRule,
|
|
141
|
+
GCPKMSKeyRotationRule,
|
|
142
|
+
GCPServiceAccountKeyRule,
|
|
143
|
+
GCPAuditLoggingRule,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
registry.register_class(GCSBucketPublicAccessRule)
|
|
147
|
+
registry.register_class(GCSBucketEncryptionRule)
|
|
148
|
+
registry.register_class(GCPFirewallOpenIngressRule)
|
|
149
|
+
registry.register_class(GCPCloudSQLEncryptionRule)
|
|
150
|
+
registry.register_class(GCPKMSKeyRotationRule)
|
|
151
|
+
registry.register_class(GCPServiceAccountKeyRule)
|
|
152
|
+
registry.register_class(GCPAuditLoggingRule)
|
|
153
|
+
|
|
154
|
+
# Kubernetes Rules
|
|
155
|
+
from security_use.iac.rules.kubernetes import (
|
|
156
|
+
K8sRunAsRootRule,
|
|
157
|
+
K8sPrivilegedContainerRule,
|
|
158
|
+
K8sResourceLimitsRule,
|
|
159
|
+
K8sHostNetworkRule,
|
|
160
|
+
K8sSecretsEnvVarsRule,
|
|
161
|
+
K8sReadOnlyRootFilesystemRule,
|
|
162
|
+
K8sNetworkPolicyRule,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
registry.register_class(K8sRunAsRootRule)
|
|
166
|
+
registry.register_class(K8sPrivilegedContainerRule)
|
|
167
|
+
registry.register_class(K8sResourceLimitsRule)
|
|
168
|
+
registry.register_class(K8sHostNetworkRule)
|
|
169
|
+
registry.register_class(K8sSecretsEnvVarsRule)
|
|
170
|
+
registry.register_class(K8sReadOnlyRootFilesystemRule)
|
|
171
|
+
registry.register_class(K8sNetworkPolicyRule)
|
security_use/parsers/__init__.py
CHANGED
|
@@ -5,6 +5,13 @@ from security_use.parsers.requirements import RequirementsParser
|
|
|
5
5
|
from security_use.parsers.pyproject import PyProjectParser
|
|
6
6
|
from security_use.parsers.pipfile import PipfileParser
|
|
7
7
|
from security_use.parsers.poetry_lock import PoetryLockParser
|
|
8
|
+
from security_use.parsers.maven import MavenParser
|
|
9
|
+
from security_use.parsers.npm import NpmParser, NpmLockParser
|
|
10
|
+
from security_use.parsers.gradle import GradleParser
|
|
11
|
+
from security_use.parsers.yarn import YarnLockParser, PnpmLockParser
|
|
12
|
+
from security_use.parsers.dotnet import CsprojParser, PackagesConfigParser
|
|
13
|
+
from security_use.parsers.conda import CondaEnvironmentParser
|
|
14
|
+
from security_use.parsers.composer import ComposerParser, ComposerLockParser
|
|
8
15
|
|
|
9
16
|
__all__ = [
|
|
10
17
|
"Dependency",
|
|
@@ -13,4 +20,15 @@ __all__ = [
|
|
|
13
20
|
"PyProjectParser",
|
|
14
21
|
"PipfileParser",
|
|
15
22
|
"PoetryLockParser",
|
|
23
|
+
"MavenParser",
|
|
24
|
+
"NpmParser",
|
|
25
|
+
"NpmLockParser",
|
|
26
|
+
"GradleParser",
|
|
27
|
+
"YarnLockParser",
|
|
28
|
+
"PnpmLockParser",
|
|
29
|
+
"CsprojParser",
|
|
30
|
+
"PackagesConfigParser",
|
|
31
|
+
"CondaEnvironmentParser",
|
|
32
|
+
"ComposerParser",
|
|
33
|
+
"ComposerLockParser",
|
|
16
34
|
]
|
security_use/parsers/base.py
CHANGED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Parser for PHP Composer files (composer.json, composer.lock)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from security_use.parsers.base import Dependency, DependencyParser
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ComposerParser(DependencyParser):
|
|
10
|
+
"""Parser for composer.json files."""
|
|
11
|
+
|
|
12
|
+
def parse(self, content: str) -> list[Dependency]:
|
|
13
|
+
"""Parse composer.json content."""
|
|
14
|
+
dependencies = []
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
data = json.loads(content)
|
|
18
|
+
except json.JSONDecodeError:
|
|
19
|
+
return dependencies
|
|
20
|
+
|
|
21
|
+
# Parse require and require-dev sections
|
|
22
|
+
for section in ["require", "require-dev"]:
|
|
23
|
+
deps = data.get(section, {})
|
|
24
|
+
if isinstance(deps, dict):
|
|
25
|
+
for name, version_spec in deps.items():
|
|
26
|
+
# Skip PHP and extensions
|
|
27
|
+
if name == "php" or name.startswith("ext-"):
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
# Extract version from spec
|
|
31
|
+
version = self._extract_version(version_spec)
|
|
32
|
+
|
|
33
|
+
dependencies.append(
|
|
34
|
+
Dependency(
|
|
35
|
+
name=name,
|
|
36
|
+
version=version,
|
|
37
|
+
version_spec=version_spec,
|
|
38
|
+
ecosystem="Packagist",
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return dependencies
|
|
43
|
+
|
|
44
|
+
def _extract_version(self, spec: str) -> Optional[str]:
|
|
45
|
+
"""Extract a concrete version from a version specification."""
|
|
46
|
+
# Handle various Composer version formats
|
|
47
|
+
# ^1.0, ~1.0, >=1.0, 1.0.*, 1.0.0, etc.
|
|
48
|
+
import re
|
|
49
|
+
|
|
50
|
+
# Try to find a version number
|
|
51
|
+
match = re.search(r"(\d+\.\d+(?:\.\d+)?)", spec)
|
|
52
|
+
if match:
|
|
53
|
+
return match.group(1)
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def supported_filenames(cls) -> list[str]:
|
|
58
|
+
"""Return supported filenames."""
|
|
59
|
+
return ["composer.json"]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ComposerLockParser(DependencyParser):
|
|
63
|
+
"""Parser for composer.lock files."""
|
|
64
|
+
|
|
65
|
+
def parse(self, content: str) -> list[Dependency]:
|
|
66
|
+
"""Parse composer.lock content."""
|
|
67
|
+
dependencies = []
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
data = json.loads(content)
|
|
71
|
+
except json.JSONDecodeError:
|
|
72
|
+
return dependencies
|
|
73
|
+
|
|
74
|
+
# Parse packages and packages-dev sections
|
|
75
|
+
for section in ["packages", "packages-dev"]:
|
|
76
|
+
packages = data.get(section, [])
|
|
77
|
+
if isinstance(packages, list):
|
|
78
|
+
for pkg in packages:
|
|
79
|
+
name = pkg.get("name")
|
|
80
|
+
version = pkg.get("version")
|
|
81
|
+
|
|
82
|
+
if name:
|
|
83
|
+
# Remove 'v' prefix if present
|
|
84
|
+
if version and version.startswith("v"):
|
|
85
|
+
version = version[1:]
|
|
86
|
+
|
|
87
|
+
dependencies.append(
|
|
88
|
+
Dependency(
|
|
89
|
+
name=name,
|
|
90
|
+
version=version,
|
|
91
|
+
version_spec=f"={version}" if version else None,
|
|
92
|
+
ecosystem="Packagist",
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return dependencies
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def supported_filenames(cls) -> list[str]:
|
|
100
|
+
"""Return supported filenames."""
|
|
101
|
+
return ["composer.lock"]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Parser for Conda environment files (environment.yml)."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from security_use.parsers.base import Dependency, DependencyParser
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CondaEnvironmentParser(DependencyParser):
|
|
10
|
+
"""Parser for Conda environment.yml files."""
|
|
11
|
+
|
|
12
|
+
# Regex for conda dependency format: package=version or package==version
|
|
13
|
+
DEP_RE = re.compile(r"^\s*-\s*(?P<name>[a-zA-Z0-9_-]+)(?:[=<>]+(?P<version>[^\s#]+))?")
|
|
14
|
+
|
|
15
|
+
def parse(self, content: str) -> list[Dependency]:
|
|
16
|
+
"""Parse environment.yml content."""
|
|
17
|
+
dependencies = []
|
|
18
|
+
in_dependencies = False
|
|
19
|
+
in_pip = False
|
|
20
|
+
|
|
21
|
+
for line_num, line in enumerate(content.splitlines(), start=1):
|
|
22
|
+
stripped = line.strip()
|
|
23
|
+
|
|
24
|
+
# Track which section we're in
|
|
25
|
+
if stripped == "dependencies:":
|
|
26
|
+
in_dependencies = True
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
if stripped.startswith("- pip:"):
|
|
30
|
+
in_pip = True
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
# Exit dependencies section
|
|
34
|
+
if stripped and not stripped.startswith("-") and ":" in stripped:
|
|
35
|
+
in_dependencies = False
|
|
36
|
+
in_pip = False
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
if not in_dependencies:
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
# Parse pip dependencies (these are PyPI packages)
|
|
43
|
+
if in_pip:
|
|
44
|
+
dep = self._parse_pip_dep(line, line_num)
|
|
45
|
+
if dep:
|
|
46
|
+
dependencies.append(dep)
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
# Parse conda dependencies
|
|
50
|
+
match = self.DEP_RE.match(line)
|
|
51
|
+
if match:
|
|
52
|
+
name = match.group("name")
|
|
53
|
+
version = match.group("version")
|
|
54
|
+
|
|
55
|
+
# Skip python itself and other non-package entries
|
|
56
|
+
if name.lower() in ("python", "pip"):
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
dependencies.append(
|
|
60
|
+
Dependency(
|
|
61
|
+
name=name,
|
|
62
|
+
version=version,
|
|
63
|
+
version_spec=f"={version}" if version else None,
|
|
64
|
+
line_number=line_num,
|
|
65
|
+
ecosystem="conda",
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return dependencies
|
|
70
|
+
|
|
71
|
+
def _parse_pip_dep(self, line: str, line_num: int) -> Optional[Dependency]:
|
|
72
|
+
"""Parse a pip dependency line from conda environment file."""
|
|
73
|
+
# Format: - package==version or - package>=version
|
|
74
|
+
match = re.match(r"^\s*-\s*(?P<name>[a-zA-Z0-9_-]+)(?P<spec>[=<>!]+(?P<version>[^\s#]+))?", line)
|
|
75
|
+
if match:
|
|
76
|
+
name = match.group("name")
|
|
77
|
+
version = match.group("version")
|
|
78
|
+
spec = match.group("spec")
|
|
79
|
+
|
|
80
|
+
return Dependency(
|
|
81
|
+
name=name,
|
|
82
|
+
version=version,
|
|
83
|
+
version_spec=spec,
|
|
84
|
+
line_number=line_num,
|
|
85
|
+
ecosystem="PyPI",
|
|
86
|
+
)
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def supported_filenames(cls) -> list[str]:
|
|
91
|
+
"""Return supported filenames."""
|
|
92
|
+
return [
|
|
93
|
+
"environment.yml",
|
|
94
|
+
"environment.yaml",
|
|
95
|
+
"conda-environment.yml",
|
|
96
|
+
"conda-environment.yaml",
|
|
97
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Parser for .NET project files (*.csproj, packages.config)."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import xml.etree.ElementTree as ET
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from security_use.parsers.base import Dependency, DependencyParser
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CsprojParser(DependencyParser):
|
|
11
|
+
"""Parser for .NET .csproj and .fsproj files."""
|
|
12
|
+
|
|
13
|
+
def parse(self, content: str) -> list[Dependency]:
|
|
14
|
+
"""Parse .csproj/.fsproj content."""
|
|
15
|
+
dependencies = []
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
root = ET.fromstring(content)
|
|
19
|
+
except ET.ParseError:
|
|
20
|
+
return dependencies
|
|
21
|
+
|
|
22
|
+
# Find PackageReference elements
|
|
23
|
+
for item_group in root.iter():
|
|
24
|
+
if item_group.tag == "PackageReference":
|
|
25
|
+
name = item_group.get("Include")
|
|
26
|
+
version = item_group.get("Version")
|
|
27
|
+
|
|
28
|
+
if name:
|
|
29
|
+
# Version might be in a child element
|
|
30
|
+
if not version:
|
|
31
|
+
version_elem = item_group.find("Version")
|
|
32
|
+
if version_elem is not None:
|
|
33
|
+
version = version_elem.text
|
|
34
|
+
|
|
35
|
+
dependencies.append(
|
|
36
|
+
Dependency(
|
|
37
|
+
name=name,
|
|
38
|
+
version=version,
|
|
39
|
+
version_spec=f"={version}" if version else None,
|
|
40
|
+
ecosystem="NuGet",
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return dependencies
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def supported_filenames(cls) -> list[str]:
|
|
48
|
+
"""Return supported filenames."""
|
|
49
|
+
return [
|
|
50
|
+
"*.csproj",
|
|
51
|
+
"*.fsproj",
|
|
52
|
+
"*.vbproj",
|
|
53
|
+
"Directory.Packages.props",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PackagesConfigParser(DependencyParser):
|
|
58
|
+
"""Parser for NuGet packages.config files."""
|
|
59
|
+
|
|
60
|
+
def parse(self, content: str) -> list[Dependency]:
|
|
61
|
+
"""Parse packages.config content."""
|
|
62
|
+
dependencies = []
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
root = ET.fromstring(content)
|
|
66
|
+
except ET.ParseError:
|
|
67
|
+
return dependencies
|
|
68
|
+
|
|
69
|
+
# Find package elements
|
|
70
|
+
for package in root.iter("package"):
|
|
71
|
+
name = package.get("id")
|
|
72
|
+
version = package.get("version")
|
|
73
|
+
|
|
74
|
+
if name:
|
|
75
|
+
dependencies.append(
|
|
76
|
+
Dependency(
|
|
77
|
+
name=name,
|
|
78
|
+
version=version,
|
|
79
|
+
version_spec=f"={version}" if version else None,
|
|
80
|
+
ecosystem="NuGet",
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return dependencies
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def supported_filenames(cls) -> list[str]:
|
|
88
|
+
"""Return supported filenames."""
|
|
89
|
+
return ["packages.config"]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Parser for Gradle build files (build.gradle, build.gradle.kts)."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from security_use.parsers.base import Dependency, DependencyParser
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GradleParser(DependencyParser):
|
|
10
|
+
"""Parser for Gradle build files."""
|
|
11
|
+
|
|
12
|
+
# Regex patterns for Gradle dependencies
|
|
13
|
+
# Groovy: implementation 'group:artifact:version'
|
|
14
|
+
GROOVY_DEP_RE = re.compile(
|
|
15
|
+
r"(?:implementation|api|compile|runtimeOnly|testImplementation|testCompile|compileOnly)"
|
|
16
|
+
r"\s*['\"](?P<group>[^:]+):(?P<artifact>[^:]+):(?P<version>[^'\"]+)['\"]"
|
|
17
|
+
)
|
|
18
|
+
# Groovy: implementation group: 'group', name: 'artifact', version: 'version'
|
|
19
|
+
GROOVY_MAP_RE = re.compile(
|
|
20
|
+
r"(?:implementation|api|compile|runtimeOnly|testImplementation|testCompile|compileOnly)"
|
|
21
|
+
r"\s+group:\s*['\"](?P<group>[^'\"]+)['\"],\s*"
|
|
22
|
+
r"name:\s*['\"](?P<artifact>[^'\"]+)['\"],\s*"
|
|
23
|
+
r"version:\s*['\"](?P<version>[^'\"]+)['\"]"
|
|
24
|
+
)
|
|
25
|
+
# Kotlin DSL: implementation("group:artifact:version")
|
|
26
|
+
KOTLIN_DEP_RE = re.compile(
|
|
27
|
+
r"(?:implementation|api|compile|runtimeOnly|testImplementation|testCompile|compileOnly)"
|
|
28
|
+
r"\s*\(\s*['\"](?P<group>[^:]+):(?P<artifact>[^:]+):(?P<version>[^'\"]+)['\"]\s*\)"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def parse(self, content: str) -> list[Dependency]:
|
|
32
|
+
"""Parse Gradle build file content."""
|
|
33
|
+
dependencies = []
|
|
34
|
+
|
|
35
|
+
for line_num, line in enumerate(content.splitlines(), start=1):
|
|
36
|
+
dep = self._parse_line(line, line_num)
|
|
37
|
+
if dep:
|
|
38
|
+
dependencies.append(dep)
|
|
39
|
+
|
|
40
|
+
return dependencies
|
|
41
|
+
|
|
42
|
+
def _parse_line(self, line: str, line_number: int) -> Optional[Dependency]:
|
|
43
|
+
"""Parse a single line from build.gradle."""
|
|
44
|
+
line = line.strip()
|
|
45
|
+
|
|
46
|
+
# Skip comments
|
|
47
|
+
if line.startswith("//") or line.startswith("/*") or line.startswith("*"):
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
# Try Groovy string format
|
|
51
|
+
match = self.GROOVY_DEP_RE.search(line)
|
|
52
|
+
if match:
|
|
53
|
+
return self._create_dependency(match, line_number)
|
|
54
|
+
|
|
55
|
+
# Try Groovy map format
|
|
56
|
+
match = self.GROOVY_MAP_RE.search(line)
|
|
57
|
+
if match:
|
|
58
|
+
return self._create_dependency(match, line_number)
|
|
59
|
+
|
|
60
|
+
# Try Kotlin DSL format
|
|
61
|
+
match = self.KOTLIN_DEP_RE.search(line)
|
|
62
|
+
if match:
|
|
63
|
+
return self._create_dependency(match, line_number)
|
|
64
|
+
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
def _create_dependency(self, match: re.Match, line_number: int) -> Dependency:
|
|
68
|
+
"""Create a Dependency from a regex match."""
|
|
69
|
+
group = match.group("group")
|
|
70
|
+
artifact = match.group("artifact")
|
|
71
|
+
version = match.group("version")
|
|
72
|
+
|
|
73
|
+
# For Maven/Gradle, the package name is typically group:artifact
|
|
74
|
+
name = f"{group}:{artifact}"
|
|
75
|
+
|
|
76
|
+
return Dependency(
|
|
77
|
+
name=name,
|
|
78
|
+
version=version,
|
|
79
|
+
version_spec=f"={version}",
|
|
80
|
+
line_number=line_number,
|
|
81
|
+
ecosystem="Maven", # Gradle uses Maven Central
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def supported_filenames(cls) -> list[str]:
|
|
86
|
+
"""Return supported filenames."""
|
|
87
|
+
return [
|
|
88
|
+
"build.gradle",
|
|
89
|
+
"build.gradle.kts",
|
|
90
|
+
]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Maven pom.xml parser."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import xml.etree.ElementTree as ET
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from security_use.parsers.base import Dependency, DependencyParser
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MavenParser(DependencyParser):
|
|
11
|
+
"""Parser for Maven pom.xml files."""
|
|
12
|
+
|
|
13
|
+
# Maven namespace
|
|
14
|
+
MAVEN_NS = "{http://maven.apache.org/POM/4.0.0}"
|
|
15
|
+
|
|
16
|
+
def parse(self, content: str) -> list[Dependency]:
|
|
17
|
+
"""Parse Maven pom.xml content.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
content: The pom.xml file content.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
List of dependencies found.
|
|
24
|
+
"""
|
|
25
|
+
dependencies = []
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
# Remove namespace for easier parsing
|
|
29
|
+
content_clean = re.sub(r'\sxmlns="[^"]+"', '', content, count=1)
|
|
30
|
+
root = ET.fromstring(content_clean)
|
|
31
|
+
except ET.ParseError:
|
|
32
|
+
return dependencies
|
|
33
|
+
|
|
34
|
+
# Extract properties for variable substitution
|
|
35
|
+
properties = self._extract_properties(root)
|
|
36
|
+
|
|
37
|
+
# Find all dependency elements
|
|
38
|
+
for dep_elem in root.iter("dependency"):
|
|
39
|
+
group_id = self._get_text(dep_elem, "groupId")
|
|
40
|
+
artifact_id = self._get_text(dep_elem, "artifactId")
|
|
41
|
+
version = self._get_text(dep_elem, "version")
|
|
42
|
+
|
|
43
|
+
if not group_id or not artifact_id:
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
# Resolve property references like ${project.version}
|
|
47
|
+
if version:
|
|
48
|
+
version = self._resolve_properties(version, properties)
|
|
49
|
+
|
|
50
|
+
# Skip if version still has unresolved properties
|
|
51
|
+
if version and "${" in version:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
# Create Maven coordinate as package name
|
|
55
|
+
package_name = f"{group_id}:{artifact_id}"
|
|
56
|
+
|
|
57
|
+
dependencies.append(
|
|
58
|
+
Dependency(
|
|
59
|
+
name=package_name,
|
|
60
|
+
version=version,
|
|
61
|
+
extras=None,
|
|
62
|
+
source="pom.xml",
|
|
63
|
+
ecosystem="Maven",
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return dependencies
|
|
68
|
+
|
|
69
|
+
def _extract_properties(self, root: ET.Element) -> dict[str, str]:
|
|
70
|
+
"""Extract properties from pom.xml for variable substitution."""
|
|
71
|
+
properties = {}
|
|
72
|
+
|
|
73
|
+
# Get project version
|
|
74
|
+
version_elem = root.find("version")
|
|
75
|
+
if version_elem is not None and version_elem.text:
|
|
76
|
+
properties["project.version"] = version_elem.text
|
|
77
|
+
|
|
78
|
+
# Get properties section
|
|
79
|
+
props_elem = root.find("properties")
|
|
80
|
+
if props_elem is not None:
|
|
81
|
+
for prop in props_elem:
|
|
82
|
+
tag = prop.tag.replace(self.MAVEN_NS, "")
|
|
83
|
+
if prop.text:
|
|
84
|
+
properties[tag] = prop.text
|
|
85
|
+
|
|
86
|
+
return properties
|
|
87
|
+
|
|
88
|
+
def _resolve_properties(self, value: str, properties: dict[str, str]) -> str:
|
|
89
|
+
"""Resolve ${property} references in a value."""
|
|
90
|
+
pattern = r"\$\{([^}]+)\}"
|
|
91
|
+
|
|
92
|
+
def replace(match: re.Match) -> str:
|
|
93
|
+
prop_name = match.group(1)
|
|
94
|
+
return properties.get(prop_name, match.group(0))
|
|
95
|
+
|
|
96
|
+
return re.sub(pattern, replace, value)
|
|
97
|
+
|
|
98
|
+
def _get_text(self, elem: ET.Element, tag: str) -> Optional[str]:
|
|
99
|
+
"""Get text content of a child element."""
|
|
100
|
+
child = elem.find(tag)
|
|
101
|
+
if child is not None and child.text:
|
|
102
|
+
return child.text.strip()
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def supported_filenames(cls) -> list[str]:
|
|
107
|
+
"""Return supported filenames."""
|
|
108
|
+
return ["pom.xml"]
|