ref-agents 1.0.0__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.
- ref_agents/__init__.py +9 -0
- ref_agents/api_keys.json.example +8 -0
- ref_agents/auth.py +129 -0
- ref_agents/codemap/..md +62 -0
- ref_agents/codemap/CODE_MAP.md +37 -0
- ref_agents/codemap/core.md +43 -0
- ref_agents/codemap/models.md +43 -0
- ref_agents/codemap/prompts.md +40 -0
- ref_agents/codemap/security.md +45 -0
- ref_agents/codemap/tools.md +94 -0
- ref_agents/codemap/tools_browser.md +44 -0
- ref_agents/codemap/utils.md +42 -0
- ref_agents/codemap/workflow.md +42 -0
- ref_agents/config/ai_patterns.yaml +101 -0
- ref_agents/config/frameworks/angular.yaml +104 -0
- ref_agents/config/frameworks/aspnet.yaml +84 -0
- ref_agents/config/frameworks/ef_core.yaml +81 -0
- ref_agents/config/frameworks/react.yaml +111 -0
- ref_agents/config/frameworks/spring_boot.yaml +117 -0
- ref_agents/config/languages/csharp.yaml +153 -0
- ref_agents/config/languages/java.yaml +188 -0
- ref_agents/config/languages/javascript.yaml +172 -0
- ref_agents/config/languages/python.yaml +153 -0
- ref_agents/config/languages/typescript.yaml +193 -0
- ref_agents/constants.py +553 -0
- ref_agents/core/__init__.py +15 -0
- ref_agents/core/config_loader.py +160 -0
- ref_agents/core/config_models.py +167 -0
- ref_agents/core/config_parsing.py +84 -0
- ref_agents/core/language_detector.py +388 -0
- ref_agents/core/validation_models.py +66 -0
- ref_agents/core/validation_primitives.py +176 -0
- ref_agents/errors.py +34 -0
- ref_agents/license_client.py +247 -0
- ref_agents/models/__init__.py +22 -0
- ref_agents/models/gherkin.py +45 -0
- ref_agents/models/hierarchy.py +80 -0
- ref_agents/models/invest.py +59 -0
- ref_agents/models/version.py +49 -0
- ref_agents/prompts/__init__.py +9 -0
- ref_agents/prompts/start_agent.py +772 -0
- ref_agents/rules/architecture/backend_patterns.md +43 -0
- ref_agents/rules/architecture/diagramming.md +100 -0
- ref_agents/rules/architecture/frontend_patterns.md +40 -0
- ref_agents/rules/architecture/impact_analysis.md +129 -0
- ref_agents/rules/architecture/migration_strategy.md +208 -0
- ref_agents/rules/architecture/regression_protocol.md +77 -0
- ref_agents/rules/architecture/system_design.md +97 -0
- ref_agents/rules/common/codemap_standard.md +97 -0
- ref_agents/rules/common/core_protocol.md +59 -0
- ref_agents/rules/common/prompt_engineering.md +294 -0
- ref_agents/rules/development/debugging.md +32 -0
- ref_agents/rules/development/implementation.md +205 -0
- ref_agents/rules/operations/completion.md +119 -0
- ref_agents/rules/operations/cutover_protocol.md +218 -0
- ref_agents/rules/operations/discovery.md +179 -0
- ref_agents/rules/operations/fix_workflow.md +87 -0
- ref_agents/rules/operations/forensics.md +278 -0
- ref_agents/rules/operations/platform.md +263 -0
- ref_agents/rules/operations/synchronous_flow.md +25 -0
- ref_agents/rules/product/ac_validation.md +25 -0
- ref_agents/rules/product/brainstorming.md +27 -0
- ref_agents/rules/product/ref_flow.md +101 -0
- ref_agents/rules/product/requirements_std.md +114 -0
- ref_agents/rules/product/spec_writing.md +235 -0
- ref_agents/rules/product/strategy.md +96 -0
- ref_agents/rules/quality/documentation_standards.md +46 -0
- ref_agents/rules/quality/parity_testing.md +234 -0
- ref_agents/rules/quality/project_documentation.md +56 -0
- ref_agents/rules/quality/qa_lead.md +111 -0
- ref_agents/rules/quality/test_design.md +146 -0
- ref_agents/rules/quality/testing_standards.md +293 -0
- ref_agents/rules/review/pr_review.md +116 -0
- ref_agents/rules/security/security_audit.md +83 -0
- ref_agents/security/__init__.py +22 -0
- ref_agents/security/dependency_audit.py +188 -0
- ref_agents/security/file_audit.py +208 -0
- ref_agents/security/network_scan.py +179 -0
- ref_agents/security/report_generator.py +313 -0
- ref_agents/security/secret_scan.py +252 -0
- ref_agents/security/url_scan.py +240 -0
- ref_agents/security_scan.py +236 -0
- ref_agents/server.py +1586 -0
- ref_agents/session.py +100 -0
- ref_agents/tool_names.py +55 -0
- ref_agents/tools/__init__.py +8 -0
- ref_agents/tools/agents_generator.py +315 -0
- ref_agents/tools/ai_pattern_detector.py +815 -0
- ref_agents/tools/brownfield_populator.py +529 -0
- ref_agents/tools/browser/__init__.py +50 -0
- ref_agents/tools/browser/evidence_verifier.py +302 -0
- ref_agents/tools/browser/execution_logger.py +249 -0
- ref_agents/tools/browser/playwright_mcp_client.py +259 -0
- ref_agents/tools/browser/screenshot_utils.py +184 -0
- ref_agents/tools/browser/test_executor.py +537 -0
- ref_agents/tools/code_quality_scanner.py +629 -0
- ref_agents/tools/codemap/..md +93 -0
- ref_agents/tools/codemap/CODE_MAP.md +30 -0
- ref_agents/tools/codemap/browser.md +44 -0
- ref_agents/tools/codemap.py +403 -0
- ref_agents/tools/codemap_freshness.py +234 -0
- ref_agents/tools/comment_smell_scanner.py +346 -0
- ref_agents/tools/complexity.py +436 -0
- ref_agents/tools/complexity_ast.py +333 -0
- ref_agents/tools/compliance.py +246 -0
- ref_agents/tools/compliance_remediation.py +846 -0
- ref_agents/tools/context_graph.py +839 -0
- ref_agents/tools/context_manager.py +550 -0
- ref_agents/tools/context_tools.py +121 -0
- ref_agents/tools/cross_repo_linker.py +393 -0
- ref_agents/tools/dead_code_scanner.py +637 -0
- ref_agents/tools/debt_scanner.py +1092 -0
- ref_agents/tools/dependency_graph.py +272 -0
- ref_agents/tools/discovery_audit.py +372 -0
- ref_agents/tools/docs_scanner.py +600 -0
- ref_agents/tools/evaluate_gate.py +119 -0
- ref_agents/tools/external_detector.py +524 -0
- ref_agents/tools/features_generator.py +282 -0
- ref_agents/tools/flow_gap_detector.py +373 -0
- ref_agents/tools/flow_mapper.py +327 -0
- ref_agents/tools/full_suite_runner.py +740 -0
- ref_agents/tools/gherkin_parser.py +227 -0
- ref_agents/tools/guard_tools.py +139 -0
- ref_agents/tools/handoff_tools.py +282 -0
- ref_agents/tools/health_scanner.py +1211 -0
- ref_agents/tools/hierarchy_manager.py +289 -0
- ref_agents/tools/invest_scorer.py +249 -0
- ref_agents/tools/jira_confluence_export.py +306 -0
- ref_agents/tools/json_output.py +76 -0
- ref_agents/tools/migration_mapper.py +946 -0
- ref_agents/tools/migration_readiness_scanner.py +209 -0
- ref_agents/tools/pattern_learner.py +522 -0
- ref_agents/tools/report_utils.py +155 -0
- ref_agents/tools/requirements_serializer.py +225 -0
- ref_agents/tools/security_audit_tool.py +106 -0
- ref_agents/tools/sequencing_engine.py +288 -0
- ref_agents/tools/summary_generator.py +275 -0
- ref_agents/tools/symbol_resolver.py +306 -0
- ref_agents/tools/symbol_smoke_runner.py +336 -0
- ref_agents/tools/test_plan_validator.py +189 -0
- ref_agents/tools/test_smell_walker.py +902 -0
- ref_agents/tools/tier1_fixer.py +502 -0
- ref_agents/tools/validators/__init__.py +419 -0
- ref_agents/tools/validators/architect.py +268 -0
- ref_agents/tools/validators/cutover_engineer.py +167 -0
- ref_agents/tools/validators/developer.py +180 -0
- ref_agents/tools/validators/discovery.py +150 -0
- ref_agents/tools/validators/forensic_engineer.py +191 -0
- ref_agents/tools/validators/impact_architect.py +181 -0
- ref_agents/tools/validators/migration_planner.py +166 -0
- ref_agents/tools/validators/parity_tester.py +155 -0
- ref_agents/tools/validators/platform_engineer.py +134 -0
- ref_agents/tools/validators/pr_reviewer.py +129 -0
- ref_agents/tools/validators/product_manager.py +291 -0
- ref_agents/tools/validators/qa_lead.py +172 -0
- ref_agents/tools/validators/scrum_master.py +212 -0
- ref_agents/tools/validators/security_owner.py +162 -0
- ref_agents/tools/validators/specifier.py +134 -0
- ref_agents/tools/validators/strategist.py +149 -0
- ref_agents/tools/validators/tester.py +121 -0
- ref_agents/tools/version_manager.py +202 -0
- ref_agents/tools/workflow_tools.py +1549 -0
- ref_agents/utils/__init__.py +21 -0
- ref_agents/utils/git_utils.py +351 -0
- ref_agents/utils/handoff_logger.py +368 -0
- ref_agents/utils/ignore_matcher.py +270 -0
- ref_agents/workflow/__init__.py +19 -0
- ref_agents/workflow/capabilities.py +328 -0
- ref_agents/workflow/state_machine.py +708 -0
- ref_agents/workflow/transitions.py +658 -0
- ref_agents-1.0.0.dist-info/METADATA +365 -0
- ref_agents-1.0.0.dist-info/RECORD +175 -0
- ref_agents-1.0.0.dist-info/WHEEL +4 -0
- ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
- ref_agents-1.0.0.dist-info/licenses/LICENSE +115 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Security Report Generator.
|
|
2
|
+
|
|
3
|
+
Generates professional Markdown reports for enterprise security review.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import structlog
|
|
11
|
+
|
|
12
|
+
from ref_agents.security.network_scan import NetworkScanResult
|
|
13
|
+
from ref_agents.security.url_scan import URLScanResult
|
|
14
|
+
from ref_agents.security.secret_scan import SecretScanResult
|
|
15
|
+
from ref_agents.security.dependency_audit import DependencyAuditResult
|
|
16
|
+
from ref_agents.security.file_audit import FileAuditResult
|
|
17
|
+
|
|
18
|
+
logger = structlog.get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
VERSION = "0.1.0"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class SecurityReport:
|
|
25
|
+
"""Complete security audit report."""
|
|
26
|
+
|
|
27
|
+
network_result: NetworkScanResult
|
|
28
|
+
url_result: URLScanResult
|
|
29
|
+
secret_result: SecretScanResult
|
|
30
|
+
dependency_result: DependencyAuditResult
|
|
31
|
+
file_result: FileAuditResult
|
|
32
|
+
generated_at: datetime
|
|
33
|
+
project_name: str = "REF-Agents"
|
|
34
|
+
version: str = VERSION
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def overall_passed(self) -> bool:
|
|
38
|
+
"""Check if all scans passed."""
|
|
39
|
+
return all(
|
|
40
|
+
[
|
|
41
|
+
self.network_result.passed,
|
|
42
|
+
self.url_result.passed,
|
|
43
|
+
self.secret_result.passed,
|
|
44
|
+
self.dependency_result.passed,
|
|
45
|
+
self.file_result.passed,
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _status_icon(passed: bool) -> str:
|
|
51
|
+
"""Return status icon for pass/fail."""
|
|
52
|
+
return "✅" if passed else "❌"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _generate_executive_summary(
|
|
56
|
+
report: SecurityReport, timestamp: str, overall_icon: str, overall_status: str
|
|
57
|
+
) -> str:
|
|
58
|
+
"""Generate executive summary section."""
|
|
59
|
+
return f"""# {report.project_name} Security Audit Report
|
|
60
|
+
|
|
61
|
+
**Generated:** {timestamp}
|
|
62
|
+
**Version:** {report.version}
|
|
63
|
+
**Auditor:** Automated Security Scanner v1.0
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Executive Summary
|
|
68
|
+
|
|
69
|
+
| Check | Status | Details |
|
|
70
|
+
|-------|--------|---------|
|
|
71
|
+
| Network Import Scan | {_status_icon(report.network_result.passed)} {"PASS" if report.network_result.passed else "FAIL"} | {report.network_result.summary} |
|
|
72
|
+
| Outbound URL Scan | {_status_icon(report.url_result.passed)} {"PASS" if report.url_result.passed else "FAIL"} | {report.url_result.summary} |
|
|
73
|
+
| Secret Detection | {_status_icon(report.secret_result.passed)} {"PASS" if report.secret_result.passed else "FAIL"} | {report.secret_result.summary} |
|
|
74
|
+
| Dependency Audit | {_status_icon(report.dependency_result.passed)} {"PASS" if report.dependency_result.passed else "FAIL"} | {report.dependency_result.summary} |
|
|
75
|
+
| File Operation Audit | {_status_icon(report.file_result.passed)} {"PASS" if report.file_result.passed else "FAIL"} | {report.file_result.summary} |
|
|
76
|
+
|
|
77
|
+
**OVERALL: {overall_icon} {overall_status}**
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Detailed Findings
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _generate_network_section(report: SecurityReport) -> str:
|
|
86
|
+
"""Generate network scan section."""
|
|
87
|
+
section = f"""### 1. Network Import Scan
|
|
88
|
+
**Status:** {_status_icon(report.network_result.passed)} {"PASSED" if report.network_result.passed else "FAILED"}
|
|
89
|
+
**Files Scanned:** {report.network_result.files_scanned}
|
|
90
|
+
**Network Imports Found:** {len(report.network_result.violations)}
|
|
91
|
+
|
|
92
|
+
Scanned for: requests, httpx, aiohttp, urllib, socket, http.client, etc.
|
|
93
|
+
|
|
94
|
+
"""
|
|
95
|
+
if report.network_result.violations:
|
|
96
|
+
section += "**Violations:**\n\n"
|
|
97
|
+
section += "| File | Import | Matched |\n"
|
|
98
|
+
section += "|------|--------|--------|\n"
|
|
99
|
+
for v in report.network_result.violations:
|
|
100
|
+
section += f"| {v['file']} | {v['import']} | {v['matched']} |\n"
|
|
101
|
+
section += "\n"
|
|
102
|
+
else:
|
|
103
|
+
section += "No network libraries detected in codebase.\n\n"
|
|
104
|
+
return section
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _generate_url_section(report: SecurityReport) -> str:
|
|
108
|
+
"""Generate URL scan section."""
|
|
109
|
+
section = f"""### 2. Outbound URL Scan
|
|
110
|
+
**Status:** {_status_icon(report.url_result.passed)} {"PASSED" if report.url_result.passed else "FAILED"}
|
|
111
|
+
**Files Scanned:** {report.url_result.files_scanned}
|
|
112
|
+
**External URLs Found:** {len(report.url_result.external_urls)}
|
|
113
|
+
**Whitelisted URLs:** {len(report.url_result.whitelisted_urls)}
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
if report.url_result.external_urls:
|
|
117
|
+
section += "**External URLs (require review):**\n\n"
|
|
118
|
+
section += "| File | URL | Type |\n"
|
|
119
|
+
section += "|------|-----|------|\n"
|
|
120
|
+
for u in report.url_result.external_urls[:20]: # Limit to first 20
|
|
121
|
+
section += f"| {u['file']} | {u['url'][:50]}... | {u['type']} |\n"
|
|
122
|
+
if len(report.url_result.external_urls) > 20: # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
123
|
+
section += f"\n*...and {len(report.url_result.external_urls) - 20} more*\n"
|
|
124
|
+
section += "\n"
|
|
125
|
+
else:
|
|
126
|
+
section += "No external URLs detected.\n\n"
|
|
127
|
+
return section
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _generate_secret_section(report: SecurityReport) -> str:
|
|
131
|
+
"""Generate secret scan section."""
|
|
132
|
+
section = f"""### 3. Secret Detection
|
|
133
|
+
**Status:** {_status_icon(report.secret_result.passed)} {"PASSED" if report.secret_result.passed else "FAILED"}
|
|
134
|
+
**Files Scanned:** {report.secret_result.files_scanned}
|
|
135
|
+
**Secrets Found:** {len(report.secret_result.secrets_found)}
|
|
136
|
+
**False Positives Skipped:** {report.secret_result.false_positives_skipped}
|
|
137
|
+
|
|
138
|
+
"""
|
|
139
|
+
if report.secret_result.secrets_found:
|
|
140
|
+
section += "**Potential Secrets:**\n\n"
|
|
141
|
+
section += "| File | Line | Type | Value (masked) |\n"
|
|
142
|
+
section += "|------|------|------|----------------|\n"
|
|
143
|
+
for s in report.secret_result.secrets_found[:20]:
|
|
144
|
+
section += (
|
|
145
|
+
f"| {s['file']} | {s['line']} | {s['type']} | {s['masked_value']} |\n"
|
|
146
|
+
)
|
|
147
|
+
section += "\n"
|
|
148
|
+
else:
|
|
149
|
+
section += "No hardcoded secrets detected.\n\n"
|
|
150
|
+
return section
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _generate_dependency_section(report: SecurityReport) -> str:
|
|
154
|
+
"""Generate dependency scan section."""
|
|
155
|
+
section = f"""### 4. Dependency Analysis
|
|
156
|
+
**Status:** {_status_icon(report.dependency_result.passed)} {"PASSED" if report.dependency_result.passed else "FAILED"}
|
|
157
|
+
**Total Dependencies:** {len(report.dependency_result.dependencies)}
|
|
158
|
+
|
|
159
|
+
| Package | Version | License | Safe |
|
|
160
|
+
|---------|---------|---------|------|
|
|
161
|
+
"""
|
|
162
|
+
for dep in report.dependency_result.dependencies:
|
|
163
|
+
safe_icon = _status_icon(dep.license_safe)
|
|
164
|
+
section += f"| {dep.name} | {dep.version} | {dep.license} | {safe_icon} |\n"
|
|
165
|
+
|
|
166
|
+
if report.dependency_result.unsafe_licenses:
|
|
167
|
+
section += f"\n**⚠️ Licenses requiring review:** {', '.join(report.dependency_result.unsafe_licenses)}\n"
|
|
168
|
+
return section
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _generate_file_section(report: SecurityReport) -> str:
|
|
172
|
+
"""Generate file operation scan section."""
|
|
173
|
+
section = f"""
|
|
174
|
+
|
|
175
|
+
### 5. File Operation Audit
|
|
176
|
+
**Status:** {_status_icon(report.file_result.passed)} {"PASSED" if report.file_result.passed else "FAILED"}
|
|
177
|
+
**Files Scanned:** {report.file_result.files_scanned}
|
|
178
|
+
**File Operations Found:** {len(report.file_result.operations)}
|
|
179
|
+
**Dangerous Operations:** {len(report.file_result.dangerous_operations)}
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
if report.file_result.dangerous_operations:
|
|
183
|
+
section += "**⚠️ Dangerous Operations:**\n\n"
|
|
184
|
+
section += "| File | Line | Operation | Context |\n"
|
|
185
|
+
section += "|------|------|-----------|--------|\n"
|
|
186
|
+
for op in report.file_result.dangerous_operations:
|
|
187
|
+
section += (
|
|
188
|
+
f"| {op.file} | {op.line} | {op.operation} | `{op.context[:40]}...` |\n"
|
|
189
|
+
)
|
|
190
|
+
section += "\n"
|
|
191
|
+
else:
|
|
192
|
+
section += "All file operations are safe local-only operations.\n\n"
|
|
193
|
+
return section
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _generate_certification(report: SecurityReport) -> str:
|
|
197
|
+
"""Generate certification section."""
|
|
198
|
+
section = """---
|
|
199
|
+
|
|
200
|
+
## Architecture Verification
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
┌─────────────────────────────────────────────────────────┐
|
|
204
|
+
│ CUSTOMER MACHINE │
|
|
205
|
+
│ ┌──────────────┐ stdio ┌───────────────┐ │
|
|
206
|
+
│ │ AI Client │◄───────────────►│ ref-agents │ │
|
|
207
|
+
│ │ (Cursor) │ │ (local only) │ │
|
|
208
|
+
│ └──────────────┘ └───────┬───────┘ │
|
|
209
|
+
│ │ │
|
|
210
|
+
│ ┌──────▼──────┐ │
|
|
211
|
+
│ │ Local Files │ │
|
|
212
|
+
│ └─────────────┘ │
|
|
213
|
+
│ │
|
|
214
|
+
│ ❌ NO external network calls │
|
|
215
|
+
│ ❌ NO cloud services │
|
|
216
|
+
│ ❌ NO telemetry │
|
|
217
|
+
206: └────────────────────────────────────────────────────────┘
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Certification
|
|
223
|
+
|
|
224
|
+
"""
|
|
225
|
+
if report.overall_passed:
|
|
226
|
+
section += f"""This report certifies that {report.project_name} version {report.version}:
|
|
227
|
+
|
|
228
|
+
1. ✅ Contains no code that transmits data externally
|
|
229
|
+
2. ✅ Operates entirely on the local filesystem
|
|
230
|
+
3. ✅ Uses only stdio transport (no network listeners)
|
|
231
|
+
4. ✅ Has no known security vulnerabilities in dependencies
|
|
232
|
+
5. ✅ Contains no hardcoded secrets or credentials
|
|
233
|
+
|
|
234
|
+
**Scan completed successfully.**
|
|
235
|
+
"""
|
|
236
|
+
else:
|
|
237
|
+
section += f"""⚠️ **This report indicates potential security issues that require review.**
|
|
238
|
+
|
|
239
|
+
{report.project_name} version {report.version} has the following issues:
|
|
240
|
+
|
|
241
|
+
"""
|
|
242
|
+
if not report.network_result.passed:
|
|
243
|
+
section += f"- ❌ {len(report.network_result.violations)} network import(s) found\n"
|
|
244
|
+
if not report.url_result.passed:
|
|
245
|
+
section += (
|
|
246
|
+
f"- ❌ {len(report.url_result.external_urls)} external URL(s) found\n"
|
|
247
|
+
)
|
|
248
|
+
if not report.secret_result.passed:
|
|
249
|
+
section += f"- ❌ {len(report.secret_result.secrets_found)} potential secret(s) found\n"
|
|
250
|
+
if not report.dependency_result.passed:
|
|
251
|
+
section += f"- ❌ {len(report.dependency_result.unsafe_licenses)} unsafe license(s) found\n"
|
|
252
|
+
if not report.file_result.passed:
|
|
253
|
+
section += f"- ❌ {len(report.file_result.dangerous_operations)} dangerous operation(s) found\n"
|
|
254
|
+
section += "\n**Please review the detailed findings above.**\n"
|
|
255
|
+
return section
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def generate_report(report: SecurityReport) -> str:
|
|
259
|
+
"""
|
|
260
|
+
Generate a professional Markdown security report.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
report: SecurityReport with all scan results
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Markdown formatted report string
|
|
267
|
+
"""
|
|
268
|
+
timestamp = report.generated_at.strftime("%Y-%m-%d %H:%M:%S")
|
|
269
|
+
overall_status = "PASSED" if report.overall_passed else "FAILED"
|
|
270
|
+
overall_icon = _status_icon(report.overall_passed)
|
|
271
|
+
|
|
272
|
+
sections = [
|
|
273
|
+
_generate_executive_summary(report, timestamp, overall_icon, overall_status),
|
|
274
|
+
_generate_network_section(report),
|
|
275
|
+
_generate_url_section(report),
|
|
276
|
+
_generate_secret_section(report),
|
|
277
|
+
_generate_dependency_section(report),
|
|
278
|
+
_generate_file_section(report),
|
|
279
|
+
_generate_certification(report),
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
return "".join(sections)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def save_report(report: SecurityReport, output_dir: Path | None = None) -> Path:
|
|
286
|
+
"""
|
|
287
|
+
Save the security report to disk.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
report: SecurityReport to save
|
|
291
|
+
output_dir: Directory to save report (default: {project_root}/ref-reports/security/)
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Path to the saved report
|
|
295
|
+
"""
|
|
296
|
+
if output_dir is None:
|
|
297
|
+
from ref_agents.session import SessionManager
|
|
298
|
+
|
|
299
|
+
session = SessionManager.get()
|
|
300
|
+
base_dir = session.project_root or Path.cwd()
|
|
301
|
+
output_dir = base_dir / "ref-reports" / "security"
|
|
302
|
+
|
|
303
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
304
|
+
|
|
305
|
+
date_str = report.generated_at.strftime("%Y-%m-%d")
|
|
306
|
+
filename = f"security_audit_{date_str}.md"
|
|
307
|
+
output_path = output_dir / filename
|
|
308
|
+
|
|
309
|
+
report_content = generate_report(report)
|
|
310
|
+
output_path.write_text(report_content, encoding="utf-8")
|
|
311
|
+
|
|
312
|
+
logger.info("report_saved", path=str(output_path))
|
|
313
|
+
return output_path
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Secret Detection Scanner.
|
|
2
|
+
|
|
3
|
+
Scans files for hardcoded secrets, API keys, passwords, and tokens
|
|
4
|
+
that could pose a security risk.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import structlog
|
|
12
|
+
|
|
13
|
+
logger = structlog.get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
# Directories to skip during scanning
|
|
16
|
+
IGNORED_DIRS: set[str] = {
|
|
17
|
+
".venv",
|
|
18
|
+
"venv",
|
|
19
|
+
".env",
|
|
20
|
+
"node_modules",
|
|
21
|
+
".git",
|
|
22
|
+
"dist",
|
|
23
|
+
"build",
|
|
24
|
+
".tox",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _should_skip(path: Path) -> bool:
|
|
29
|
+
"""Check if path should be skipped."""
|
|
30
|
+
for part in path.parts:
|
|
31
|
+
if part in IGNORED_DIRS:
|
|
32
|
+
return True
|
|
33
|
+
if part.endswith("_cache") or part.endswith("cache__"):
|
|
34
|
+
return True
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Patterns to detect various types of secrets
|
|
39
|
+
SECRET_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
|
|
40
|
+
# API Keys and Tokens
|
|
41
|
+
(
|
|
42
|
+
"API Key Assignment",
|
|
43
|
+
re.compile(r'(?i)(api[_-]?key|apikey)\s*[=:]\s*["\'][a-zA-Z0-9_\-]{16,}["\']'),
|
|
44
|
+
),
|
|
45
|
+
(
|
|
46
|
+
"Secret/Password Assignment",
|
|
47
|
+
re.compile(r'(?i)(secret|password|passwd|pwd)\s*[=:]\s*["\'][^"\']{8,}["\']'),
|
|
48
|
+
),
|
|
49
|
+
(
|
|
50
|
+
"Token Assignment",
|
|
51
|
+
re.compile(
|
|
52
|
+
r'(?i)(token|auth_token|access_token)\s*[=:]\s*["\'][^"\']{16,}["\']'
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
(
|
|
56
|
+
"Bearer Token",
|
|
57
|
+
re.compile(r"(?i)bearer\s+[a-zA-Z0-9_\-\.]{20,}"),
|
|
58
|
+
),
|
|
59
|
+
# Platform-specific tokens
|
|
60
|
+
(
|
|
61
|
+
"GitHub Personal Access Token",
|
|
62
|
+
re.compile(r"ghp_[a-zA-Z0-9]{36}"),
|
|
63
|
+
),
|
|
64
|
+
(
|
|
65
|
+
"GitHub OAuth Token",
|
|
66
|
+
re.compile(r"gho_[a-zA-Z0-9]{36}"),
|
|
67
|
+
),
|
|
68
|
+
(
|
|
69
|
+
"OpenAI API Key",
|
|
70
|
+
re.compile(r"sk-[a-zA-Z0-9]{32,}"),
|
|
71
|
+
),
|
|
72
|
+
(
|
|
73
|
+
"AWS Access Key",
|
|
74
|
+
re.compile(r"AKIA[0-9A-Z]{16}"),
|
|
75
|
+
),
|
|
76
|
+
(
|
|
77
|
+
"AWS Secret Key",
|
|
78
|
+
re.compile(r'(?i)aws_secret_access_key\s*[=:]\s*["\'][^"\']{40}["\']'),
|
|
79
|
+
),
|
|
80
|
+
# Generic high-entropy strings that look like secrets
|
|
81
|
+
(
|
|
82
|
+
"Private Key Header",
|
|
83
|
+
re.compile(r"-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----"),
|
|
84
|
+
),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
# Patterns that indicate false positives (test data, examples, etc.)
|
|
88
|
+
FALSE_POSITIVE_PATTERNS: list[re.Pattern[str]] = [
|
|
89
|
+
re.compile(r"example", re.IGNORECASE),
|
|
90
|
+
re.compile(r"your[_-]?(api[_-]?)?key", re.IGNORECASE),
|
|
91
|
+
re.compile(r"placeholder", re.IGNORECASE),
|
|
92
|
+
re.compile(r"<[a-z_]+>", re.IGNORECASE), # <your_key_here>
|
|
93
|
+
re.compile(r"\$\{[^}]+\}"), # ${ENV_VAR}
|
|
94
|
+
re.compile(r"xxx+", re.IGNORECASE),
|
|
95
|
+
re.compile(r"test[_-]?key", re.IGNORECASE),
|
|
96
|
+
re.compile(r"dummy", re.IGNORECASE),
|
|
97
|
+
re.compile(r"sample", re.IGNORECASE),
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class SecretScanResult:
|
|
103
|
+
"""Result of secret detection scan."""
|
|
104
|
+
|
|
105
|
+
passed: bool
|
|
106
|
+
files_scanned: int
|
|
107
|
+
secrets_found: list[dict[str, str]] = field(default_factory=list)
|
|
108
|
+
false_positives_skipped: int = 0
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def summary(self) -> str:
|
|
112
|
+
"""One-line summary for CLI output."""
|
|
113
|
+
if self.passed:
|
|
114
|
+
return f"0 secrets detected in {self.files_scanned} files"
|
|
115
|
+
return f"{len(self.secrets_found)} potential secrets found"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _is_false_positive(match_text: str, line: str) -> bool:
|
|
119
|
+
"""Check if the match is likely a false positive."""
|
|
120
|
+
combined = f"{match_text} {line}"
|
|
121
|
+
return any(pattern.search(combined) for pattern in FALSE_POSITIVE_PATTERNS)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _should_scan_file(file_path: Path, include_extensions: set[str]) -> bool:
|
|
125
|
+
"""Check if file should be scanned."""
|
|
126
|
+
# Skip directories and non-matching extensions
|
|
127
|
+
if file_path.is_dir():
|
|
128
|
+
return False
|
|
129
|
+
if file_path.suffix not in include_extensions:
|
|
130
|
+
return False
|
|
131
|
+
if _should_skip(file_path):
|
|
132
|
+
return False
|
|
133
|
+
# Skip test files (they contain test data)
|
|
134
|
+
if "/tests/" in str(file_path) or "test_" in file_path.name:
|
|
135
|
+
return False
|
|
136
|
+
# Skip generated reports
|
|
137
|
+
if "ref-reports" in str(file_path):
|
|
138
|
+
return False
|
|
139
|
+
# Skip lock files
|
|
140
|
+
if file_path.name in {"uv.lock", "poetry.lock", "package-lock.json"}:
|
|
141
|
+
return False
|
|
142
|
+
# User defined ignores
|
|
143
|
+
if "GAP_ANALYSIS.md" in file_path.name:
|
|
144
|
+
return False
|
|
145
|
+
if "docs/misc" in str(file_path):
|
|
146
|
+
return False
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _process_file_secrets(
|
|
151
|
+
file_path: Path,
|
|
152
|
+
directory: Path,
|
|
153
|
+
secrets_found: list[dict[str, str]],
|
|
154
|
+
) -> int:
|
|
155
|
+
"""Process a single file for secrets.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Number of false positives skipped.
|
|
159
|
+
"""
|
|
160
|
+
skipped_count = 0
|
|
161
|
+
try:
|
|
162
|
+
content = file_path.read_text(encoding="utf-8")
|
|
163
|
+
except UnicodeDecodeError:
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
relative_path = str(file_path.relative_to(directory))
|
|
167
|
+
lines = content.split("\n")
|
|
168
|
+
|
|
169
|
+
for line_num, line in enumerate(lines, start=1):
|
|
170
|
+
for secret_type, pattern in SECRET_PATTERNS:
|
|
171
|
+
for match in pattern.finditer(line):
|
|
172
|
+
match_text = match.group(0)
|
|
173
|
+
|
|
174
|
+
if _is_false_positive(match_text, line):
|
|
175
|
+
skipped_count += 1
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
# Mask the actual secret value for reporting
|
|
179
|
+
masked = match_text[:10] + "..." if len(match_text) > 10 else "***" # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
180
|
+
|
|
181
|
+
secrets_found.append(
|
|
182
|
+
{
|
|
183
|
+
"file": relative_path,
|
|
184
|
+
"line": str(line_num),
|
|
185
|
+
"type": secret_type,
|
|
186
|
+
"masked_value": masked,
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
logger.warning(
|
|
190
|
+
"potential_secret_found",
|
|
191
|
+
file=relative_path,
|
|
192
|
+
line=line_num,
|
|
193
|
+
type=secret_type,
|
|
194
|
+
)
|
|
195
|
+
return skipped_count
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def scan_secrets(
|
|
199
|
+
directory: Path,
|
|
200
|
+
include_extensions: set[str] | None = None,
|
|
201
|
+
) -> SecretScanResult:
|
|
202
|
+
"""
|
|
203
|
+
Scan files for hardcoded secrets and credentials.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
directory: Root directory to scan
|
|
207
|
+
include_extensions: File extensions to scan
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
SecretScanResult with pass/fail status and found secrets
|
|
211
|
+
"""
|
|
212
|
+
include_extensions = include_extensions or {
|
|
213
|
+
".py",
|
|
214
|
+
".js",
|
|
215
|
+
".ts",
|
|
216
|
+
".json",
|
|
217
|
+
".yaml",
|
|
218
|
+
".yml",
|
|
219
|
+
".toml",
|
|
220
|
+
".env",
|
|
221
|
+
".conf",
|
|
222
|
+
".cfg",
|
|
223
|
+
}
|
|
224
|
+
secrets_found: list[dict[str, str]] = []
|
|
225
|
+
false_positives_skipped = 0
|
|
226
|
+
files_scanned = 0
|
|
227
|
+
|
|
228
|
+
for file_path in directory.rglob("*"):
|
|
229
|
+
if not _should_scan_file(file_path, include_extensions):
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
files_scanned += 1
|
|
233
|
+
false_positives_skipped += _process_file_secrets(
|
|
234
|
+
file_path, directory, secrets_found
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
passed = len(secrets_found) == 0
|
|
238
|
+
|
|
239
|
+
logger.info(
|
|
240
|
+
"secret_scan_complete",
|
|
241
|
+
passed=passed,
|
|
242
|
+
files_scanned=files_scanned,
|
|
243
|
+
secrets_count=len(secrets_found),
|
|
244
|
+
false_positives_skipped=false_positives_skipped,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return SecretScanResult(
|
|
248
|
+
passed=passed,
|
|
249
|
+
files_scanned=files_scanned,
|
|
250
|
+
secrets_found=secrets_found,
|
|
251
|
+
false_positives_skipped=false_positives_skipped,
|
|
252
|
+
)
|