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.
Files changed (45) hide show
  1. security_use/__init__.py +9 -1
  2. security_use/auth/__init__.py +16 -0
  3. security_use/auth/client.py +223 -0
  4. security_use/auth/config.py +177 -0
  5. security_use/auth/oauth.py +317 -0
  6. security_use/cli.py +699 -34
  7. security_use/compliance/__init__.py +10 -0
  8. security_use/compliance/mapper.py +275 -0
  9. security_use/compliance/models.py +50 -0
  10. security_use/dependency_scanner.py +76 -30
  11. security_use/fixers/iac_fixer.py +173 -95
  12. security_use/iac/rules/azure.py +246 -0
  13. security_use/iac/rules/gcp.py +255 -0
  14. security_use/iac/rules/kubernetes.py +429 -0
  15. security_use/iac/rules/registry.py +56 -0
  16. security_use/parsers/__init__.py +18 -0
  17. security_use/parsers/base.py +2 -0
  18. security_use/parsers/composer.py +101 -0
  19. security_use/parsers/conda.py +97 -0
  20. security_use/parsers/dotnet.py +89 -0
  21. security_use/parsers/gradle.py +90 -0
  22. security_use/parsers/maven.py +108 -0
  23. security_use/parsers/npm.py +196 -0
  24. security_use/parsers/yarn.py +108 -0
  25. security_use/reporter.py +29 -1
  26. security_use/sbom/__init__.py +10 -0
  27. security_use/sbom/generator.py +340 -0
  28. security_use/sbom/models.py +40 -0
  29. security_use/scanner.py +15 -2
  30. security_use/sensor/__init__.py +125 -0
  31. security_use/sensor/alert_queue.py +207 -0
  32. security_use/sensor/config.py +217 -0
  33. security_use/sensor/dashboard_alerter.py +246 -0
  34. security_use/sensor/detector.py +415 -0
  35. security_use/sensor/endpoint_analyzer.py +339 -0
  36. security_use/sensor/middleware.py +521 -0
  37. security_use/sensor/models.py +140 -0
  38. security_use/sensor/webhook.py +227 -0
  39. security_use-0.2.9.dist-info/METADATA +531 -0
  40. security_use-0.2.9.dist-info/RECORD +60 -0
  41. security_use-0.2.9.dist-info/licenses/LICENSE +21 -0
  42. security_use-0.1.1.dist-info/METADATA +0 -92
  43. security_use-0.1.1.dist-info/RECORD +0 -30
  44. {security_use-0.1.1.dist-info → security_use-0.2.9.dist-info}/WHEEL +0 -0
  45. {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)
@@ -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
  ]
@@ -14,6 +14,8 @@ class Dependency:
14
14
  version_spec: Optional[str] = None
15
15
  line_number: Optional[int] = None
16
16
  extras: list[str] | None = None
17
+ source: Optional[str] = None
18
+ ecosystem: str = "PyPI"
17
19
 
18
20
  @property
19
21
  def normalized_name(self) -> str:
@@ -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"]