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.
Files changed (175) hide show
  1. ref_agents/__init__.py +9 -0
  2. ref_agents/api_keys.json.example +8 -0
  3. ref_agents/auth.py +129 -0
  4. ref_agents/codemap/..md +62 -0
  5. ref_agents/codemap/CODE_MAP.md +37 -0
  6. ref_agents/codemap/core.md +43 -0
  7. ref_agents/codemap/models.md +43 -0
  8. ref_agents/codemap/prompts.md +40 -0
  9. ref_agents/codemap/security.md +45 -0
  10. ref_agents/codemap/tools.md +94 -0
  11. ref_agents/codemap/tools_browser.md +44 -0
  12. ref_agents/codemap/utils.md +42 -0
  13. ref_agents/codemap/workflow.md +42 -0
  14. ref_agents/config/ai_patterns.yaml +101 -0
  15. ref_agents/config/frameworks/angular.yaml +104 -0
  16. ref_agents/config/frameworks/aspnet.yaml +84 -0
  17. ref_agents/config/frameworks/ef_core.yaml +81 -0
  18. ref_agents/config/frameworks/react.yaml +111 -0
  19. ref_agents/config/frameworks/spring_boot.yaml +117 -0
  20. ref_agents/config/languages/csharp.yaml +153 -0
  21. ref_agents/config/languages/java.yaml +188 -0
  22. ref_agents/config/languages/javascript.yaml +172 -0
  23. ref_agents/config/languages/python.yaml +153 -0
  24. ref_agents/config/languages/typescript.yaml +193 -0
  25. ref_agents/constants.py +553 -0
  26. ref_agents/core/__init__.py +15 -0
  27. ref_agents/core/config_loader.py +160 -0
  28. ref_agents/core/config_models.py +167 -0
  29. ref_agents/core/config_parsing.py +84 -0
  30. ref_agents/core/language_detector.py +388 -0
  31. ref_agents/core/validation_models.py +66 -0
  32. ref_agents/core/validation_primitives.py +176 -0
  33. ref_agents/errors.py +34 -0
  34. ref_agents/license_client.py +247 -0
  35. ref_agents/models/__init__.py +22 -0
  36. ref_agents/models/gherkin.py +45 -0
  37. ref_agents/models/hierarchy.py +80 -0
  38. ref_agents/models/invest.py +59 -0
  39. ref_agents/models/version.py +49 -0
  40. ref_agents/prompts/__init__.py +9 -0
  41. ref_agents/prompts/start_agent.py +772 -0
  42. ref_agents/rules/architecture/backend_patterns.md +43 -0
  43. ref_agents/rules/architecture/diagramming.md +100 -0
  44. ref_agents/rules/architecture/frontend_patterns.md +40 -0
  45. ref_agents/rules/architecture/impact_analysis.md +129 -0
  46. ref_agents/rules/architecture/migration_strategy.md +208 -0
  47. ref_agents/rules/architecture/regression_protocol.md +77 -0
  48. ref_agents/rules/architecture/system_design.md +97 -0
  49. ref_agents/rules/common/codemap_standard.md +97 -0
  50. ref_agents/rules/common/core_protocol.md +59 -0
  51. ref_agents/rules/common/prompt_engineering.md +294 -0
  52. ref_agents/rules/development/debugging.md +32 -0
  53. ref_agents/rules/development/implementation.md +205 -0
  54. ref_agents/rules/operations/completion.md +119 -0
  55. ref_agents/rules/operations/cutover_protocol.md +218 -0
  56. ref_agents/rules/operations/discovery.md +179 -0
  57. ref_agents/rules/operations/fix_workflow.md +87 -0
  58. ref_agents/rules/operations/forensics.md +278 -0
  59. ref_agents/rules/operations/platform.md +263 -0
  60. ref_agents/rules/operations/synchronous_flow.md +25 -0
  61. ref_agents/rules/product/ac_validation.md +25 -0
  62. ref_agents/rules/product/brainstorming.md +27 -0
  63. ref_agents/rules/product/ref_flow.md +101 -0
  64. ref_agents/rules/product/requirements_std.md +114 -0
  65. ref_agents/rules/product/spec_writing.md +235 -0
  66. ref_agents/rules/product/strategy.md +96 -0
  67. ref_agents/rules/quality/documentation_standards.md +46 -0
  68. ref_agents/rules/quality/parity_testing.md +234 -0
  69. ref_agents/rules/quality/project_documentation.md +56 -0
  70. ref_agents/rules/quality/qa_lead.md +111 -0
  71. ref_agents/rules/quality/test_design.md +146 -0
  72. ref_agents/rules/quality/testing_standards.md +293 -0
  73. ref_agents/rules/review/pr_review.md +116 -0
  74. ref_agents/rules/security/security_audit.md +83 -0
  75. ref_agents/security/__init__.py +22 -0
  76. ref_agents/security/dependency_audit.py +188 -0
  77. ref_agents/security/file_audit.py +208 -0
  78. ref_agents/security/network_scan.py +179 -0
  79. ref_agents/security/report_generator.py +313 -0
  80. ref_agents/security/secret_scan.py +252 -0
  81. ref_agents/security/url_scan.py +240 -0
  82. ref_agents/security_scan.py +236 -0
  83. ref_agents/server.py +1586 -0
  84. ref_agents/session.py +100 -0
  85. ref_agents/tool_names.py +55 -0
  86. ref_agents/tools/__init__.py +8 -0
  87. ref_agents/tools/agents_generator.py +315 -0
  88. ref_agents/tools/ai_pattern_detector.py +815 -0
  89. ref_agents/tools/brownfield_populator.py +529 -0
  90. ref_agents/tools/browser/__init__.py +50 -0
  91. ref_agents/tools/browser/evidence_verifier.py +302 -0
  92. ref_agents/tools/browser/execution_logger.py +249 -0
  93. ref_agents/tools/browser/playwright_mcp_client.py +259 -0
  94. ref_agents/tools/browser/screenshot_utils.py +184 -0
  95. ref_agents/tools/browser/test_executor.py +537 -0
  96. ref_agents/tools/code_quality_scanner.py +629 -0
  97. ref_agents/tools/codemap/..md +93 -0
  98. ref_agents/tools/codemap/CODE_MAP.md +30 -0
  99. ref_agents/tools/codemap/browser.md +44 -0
  100. ref_agents/tools/codemap.py +403 -0
  101. ref_agents/tools/codemap_freshness.py +234 -0
  102. ref_agents/tools/comment_smell_scanner.py +346 -0
  103. ref_agents/tools/complexity.py +436 -0
  104. ref_agents/tools/complexity_ast.py +333 -0
  105. ref_agents/tools/compliance.py +246 -0
  106. ref_agents/tools/compliance_remediation.py +846 -0
  107. ref_agents/tools/context_graph.py +839 -0
  108. ref_agents/tools/context_manager.py +550 -0
  109. ref_agents/tools/context_tools.py +121 -0
  110. ref_agents/tools/cross_repo_linker.py +393 -0
  111. ref_agents/tools/dead_code_scanner.py +637 -0
  112. ref_agents/tools/debt_scanner.py +1092 -0
  113. ref_agents/tools/dependency_graph.py +272 -0
  114. ref_agents/tools/discovery_audit.py +372 -0
  115. ref_agents/tools/docs_scanner.py +600 -0
  116. ref_agents/tools/evaluate_gate.py +119 -0
  117. ref_agents/tools/external_detector.py +524 -0
  118. ref_agents/tools/features_generator.py +282 -0
  119. ref_agents/tools/flow_gap_detector.py +373 -0
  120. ref_agents/tools/flow_mapper.py +327 -0
  121. ref_agents/tools/full_suite_runner.py +740 -0
  122. ref_agents/tools/gherkin_parser.py +227 -0
  123. ref_agents/tools/guard_tools.py +139 -0
  124. ref_agents/tools/handoff_tools.py +282 -0
  125. ref_agents/tools/health_scanner.py +1211 -0
  126. ref_agents/tools/hierarchy_manager.py +289 -0
  127. ref_agents/tools/invest_scorer.py +249 -0
  128. ref_agents/tools/jira_confluence_export.py +306 -0
  129. ref_agents/tools/json_output.py +76 -0
  130. ref_agents/tools/migration_mapper.py +946 -0
  131. ref_agents/tools/migration_readiness_scanner.py +209 -0
  132. ref_agents/tools/pattern_learner.py +522 -0
  133. ref_agents/tools/report_utils.py +155 -0
  134. ref_agents/tools/requirements_serializer.py +225 -0
  135. ref_agents/tools/security_audit_tool.py +106 -0
  136. ref_agents/tools/sequencing_engine.py +288 -0
  137. ref_agents/tools/summary_generator.py +275 -0
  138. ref_agents/tools/symbol_resolver.py +306 -0
  139. ref_agents/tools/symbol_smoke_runner.py +336 -0
  140. ref_agents/tools/test_plan_validator.py +189 -0
  141. ref_agents/tools/test_smell_walker.py +902 -0
  142. ref_agents/tools/tier1_fixer.py +502 -0
  143. ref_agents/tools/validators/__init__.py +419 -0
  144. ref_agents/tools/validators/architect.py +268 -0
  145. ref_agents/tools/validators/cutover_engineer.py +167 -0
  146. ref_agents/tools/validators/developer.py +180 -0
  147. ref_agents/tools/validators/discovery.py +150 -0
  148. ref_agents/tools/validators/forensic_engineer.py +191 -0
  149. ref_agents/tools/validators/impact_architect.py +181 -0
  150. ref_agents/tools/validators/migration_planner.py +166 -0
  151. ref_agents/tools/validators/parity_tester.py +155 -0
  152. ref_agents/tools/validators/platform_engineer.py +134 -0
  153. ref_agents/tools/validators/pr_reviewer.py +129 -0
  154. ref_agents/tools/validators/product_manager.py +291 -0
  155. ref_agents/tools/validators/qa_lead.py +172 -0
  156. ref_agents/tools/validators/scrum_master.py +212 -0
  157. ref_agents/tools/validators/security_owner.py +162 -0
  158. ref_agents/tools/validators/specifier.py +134 -0
  159. ref_agents/tools/validators/strategist.py +149 -0
  160. ref_agents/tools/validators/tester.py +121 -0
  161. ref_agents/tools/version_manager.py +202 -0
  162. ref_agents/tools/workflow_tools.py +1549 -0
  163. ref_agents/utils/__init__.py +21 -0
  164. ref_agents/utils/git_utils.py +351 -0
  165. ref_agents/utils/handoff_logger.py +368 -0
  166. ref_agents/utils/ignore_matcher.py +270 -0
  167. ref_agents/workflow/__init__.py +19 -0
  168. ref_agents/workflow/capabilities.py +328 -0
  169. ref_agents/workflow/state_machine.py +708 -0
  170. ref_agents/workflow/transitions.py +658 -0
  171. ref_agents-1.0.0.dist-info/METADATA +365 -0
  172. ref_agents-1.0.0.dist-info/RECORD +175 -0
  173. ref_agents-1.0.0.dist-info/WHEEL +4 -0
  174. ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
  175. 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
+ )