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.
@@ -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
@@ -0,0 +1,6 @@
1
+ """Fixer modules for security remediation."""
2
+
3
+ from security_use.fixers.dependency_fixer import DependencyFixer
4
+ from security_use.fixers.iac_fixer import IaCFixer
5
+
6
+ __all__ = ["DependencyFixer", "IaCFixer"]