@eddacraft/anvil-runtime 0.1.0

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 (170) hide show
  1. package/LICENSE +14 -0
  2. package/dist/cache/cache-key.d.ts +45 -0
  3. package/dist/cache/cache-key.d.ts.map +1 -0
  4. package/dist/cache/cache-key.js +135 -0
  5. package/dist/cache/index.d.ts +27 -0
  6. package/dist/cache/index.d.ts.map +1 -0
  7. package/dist/cache/index.js +38 -0
  8. package/dist/cache/providers/file-cache.d.ts +63 -0
  9. package/dist/cache/providers/file-cache.d.ts.map +1 -0
  10. package/dist/cache/providers/file-cache.js +369 -0
  11. package/dist/cache/providers/memory-cache.d.ts +52 -0
  12. package/dist/cache/providers/memory-cache.d.ts.map +1 -0
  13. package/dist/cache/providers/memory-cache.js +197 -0
  14. package/dist/cache/providers/null-cache.d.ts +26 -0
  15. package/dist/cache/providers/null-cache.d.ts.map +1 -0
  16. package/dist/cache/providers/null-cache.js +50 -0
  17. package/dist/cache/types.d.ts +114 -0
  18. package/dist/cache/types.d.ts.map +1 -0
  19. package/dist/cache/types.js +4 -0
  20. package/dist/concurrency/agent.d.ts +137 -0
  21. package/dist/concurrency/agent.d.ts.map +1 -0
  22. package/dist/concurrency/agent.js +440 -0
  23. package/dist/concurrency/atomic.d.ts +93 -0
  24. package/dist/concurrency/atomic.d.ts.map +1 -0
  25. package/dist/concurrency/atomic.js +281 -0
  26. package/dist/concurrency/git-agent.d.ts +114 -0
  27. package/dist/concurrency/git-agent.d.ts.map +1 -0
  28. package/dist/concurrency/git-agent.js +313 -0
  29. package/dist/concurrency/index.d.ts +95 -0
  30. package/dist/concurrency/index.d.ts.map +1 -0
  31. package/dist/concurrency/index.js +127 -0
  32. package/dist/concurrency/lock-manager.d.ts +170 -0
  33. package/dist/concurrency/lock-manager.d.ts.map +1 -0
  34. package/dist/concurrency/lock-manager.js +525 -0
  35. package/dist/concurrency/queue-manager.d.ts +166 -0
  36. package/dist/concurrency/queue-manager.d.ts.map +1 -0
  37. package/dist/concurrency/queue-manager.js +442 -0
  38. package/dist/concurrency/types.d.ts +382 -0
  39. package/dist/concurrency/types.d.ts.map +1 -0
  40. package/dist/concurrency/types.js +204 -0
  41. package/dist/export/constraint-collector.d.ts +175 -0
  42. package/dist/export/constraint-collector.d.ts.map +1 -0
  43. package/dist/export/constraint-collector.js +203 -0
  44. package/dist/export/formatters/llms-txt-formatter.d.ts +89 -0
  45. package/dist/export/formatters/llms-txt-formatter.d.ts.map +1 -0
  46. package/dist/export/formatters/llms-txt-formatter.js +249 -0
  47. package/dist/export/formatters/mcp-resource-formatter.d.ts +186 -0
  48. package/dist/export/formatters/mcp-resource-formatter.d.ts.map +1 -0
  49. package/dist/export/formatters/mcp-resource-formatter.js +139 -0
  50. package/dist/export/formatters/prompt-formatter.d.ts +83 -0
  51. package/dist/export/formatters/prompt-formatter.d.ts.map +1 -0
  52. package/dist/export/formatters/prompt-formatter.js +256 -0
  53. package/dist/export/index.d.ts +10 -0
  54. package/dist/export/index.d.ts.map +1 -0
  55. package/dist/export/index.js +9 -0
  56. package/dist/gate/check.interface.d.ts +15 -0
  57. package/dist/gate/check.interface.d.ts.map +1 -0
  58. package/dist/gate/check.interface.js +18 -0
  59. package/dist/gate/checks/antipattern.check.d.ts +27 -0
  60. package/dist/gate/checks/antipattern.check.d.ts.map +1 -0
  61. package/dist/gate/checks/antipattern.check.js +140 -0
  62. package/dist/gate/checks/architecture/circular-detector.d.ts +33 -0
  63. package/dist/gate/checks/architecture/circular-detector.d.ts.map +1 -0
  64. package/dist/gate/checks/architecture/circular-detector.js +71 -0
  65. package/dist/gate/checks/architecture/dependency-analyzer.d.ts +81 -0
  66. package/dist/gate/checks/architecture/dependency-analyzer.d.ts.map +1 -0
  67. package/dist/gate/checks/architecture/dependency-analyzer.js +136 -0
  68. package/dist/gate/checks/architecture/layer-validator.d.ts +75 -0
  69. package/dist/gate/checks/architecture/layer-validator.d.ts.map +1 -0
  70. package/dist/gate/checks/architecture/layer-validator.js +193 -0
  71. package/dist/gate/checks/architecture.check.d.ts +56 -0
  72. package/dist/gate/checks/architecture.check.d.ts.map +1 -0
  73. package/dist/gate/checks/architecture.check.js +394 -0
  74. package/dist/gate/checks/command-safety.check.d.ts +12 -0
  75. package/dist/gate/checks/command-safety.check.d.ts.map +1 -0
  76. package/dist/gate/checks/command-safety.check.js +230 -0
  77. package/dist/gate/checks/coverage.check.d.ts +9 -0
  78. package/dist/gate/checks/coverage.check.d.ts.map +1 -0
  79. package/dist/gate/checks/coverage.check.js +81 -0
  80. package/dist/gate/checks/dependency.check.d.ts +17 -0
  81. package/dist/gate/checks/dependency.check.d.ts.map +1 -0
  82. package/dist/gate/checks/dependency.check.js +342 -0
  83. package/dist/gate/checks/eslint.check.d.ts +14 -0
  84. package/dist/gate/checks/eslint.check.d.ts.map +1 -0
  85. package/dist/gate/checks/eslint.check.js +79 -0
  86. package/dist/gate/checks/policy.check.d.ts +78 -0
  87. package/dist/gate/checks/policy.check.d.ts.map +1 -0
  88. package/dist/gate/checks/policy.check.js +457 -0
  89. package/dist/gate/checks/secret/entropy-detector.d.ts +44 -0
  90. package/dist/gate/checks/secret/entropy-detector.d.ts.map +1 -0
  91. package/dist/gate/checks/secret/entropy-detector.js +76 -0
  92. package/dist/gate/checks/secret/git-scanner.d.ts +36 -0
  93. package/dist/gate/checks/secret/git-scanner.d.ts.map +1 -0
  94. package/dist/gate/checks/secret/git-scanner.js +90 -0
  95. package/dist/gate/checks/secret/secret-patterns.d.ts +42 -0
  96. package/dist/gate/checks/secret/secret-patterns.d.ts.map +1 -0
  97. package/dist/gate/checks/secret/secret-patterns.js +137 -0
  98. package/dist/gate/checks/secret.check.d.ts +56 -0
  99. package/dist/gate/checks/secret.check.d.ts.map +1 -0
  100. package/dist/gate/checks/secret.check.js +245 -0
  101. package/dist/gate/config/command-safety-config.d.ts +5 -0
  102. package/dist/gate/config/command-safety-config.d.ts.map +1 -0
  103. package/dist/gate/config/command-safety-config.js +69 -0
  104. package/dist/gate/config/index.d.ts +2 -0
  105. package/dist/gate/config/index.d.ts.map +1 -0
  106. package/dist/gate/config/index.js +1 -0
  107. package/dist/gate/formatters/command-safety-formatter.d.ts +10 -0
  108. package/dist/gate/formatters/command-safety-formatter.d.ts.map +1 -0
  109. package/dist/gate/formatters/command-safety-formatter.js +64 -0
  110. package/dist/gate/formatters/index.d.ts +2 -0
  111. package/dist/gate/formatters/index.d.ts.map +1 -0
  112. package/dist/gate/formatters/index.js +1 -0
  113. package/dist/gate/gate-config.d.ts +44 -0
  114. package/dist/gate/gate-config.d.ts.map +1 -0
  115. package/dist/gate/gate-config.js +334 -0
  116. package/dist/gate/gate-runner.d.ts +160 -0
  117. package/dist/gate/gate-runner.d.ts.map +1 -0
  118. package/dist/gate/gate-runner.js +531 -0
  119. package/dist/gate/index.d.ts +20 -0
  120. package/dist/gate/index.d.ts.map +1 -0
  121. package/dist/gate/index.js +14 -0
  122. package/dist/gate/parsers/command-parser.d.ts +18 -0
  123. package/dist/gate/parsers/command-parser.d.ts.map +1 -0
  124. package/dist/gate/parsers/command-parser.js +363 -0
  125. package/dist/gate/parsers/index.d.ts +2 -0
  126. package/dist/gate/parsers/index.d.ts.map +1 -0
  127. package/dist/gate/parsers/index.js +1 -0
  128. package/dist/gate/policy/index.d.ts +12 -0
  129. package/dist/gate/policy/index.d.ts.map +1 -0
  130. package/dist/gate/policy/index.js +10 -0
  131. package/dist/gate/rules/default-filesystem-rules.d.ts +3 -0
  132. package/dist/gate/rules/default-filesystem-rules.d.ts.map +1 -0
  133. package/dist/gate/rules/default-filesystem-rules.js +201 -0
  134. package/dist/gate/rules/default-git-rules.d.ts +3 -0
  135. package/dist/gate/rules/default-git-rules.d.ts.map +1 -0
  136. package/dist/gate/rules/default-git-rules.js +192 -0
  137. package/dist/gate/rules/index.d.ts +5 -0
  138. package/dist/gate/rules/index.d.ts.map +1 -0
  139. package/dist/gate/rules/index.js +3 -0
  140. package/dist/gate/rules/rule-matcher.d.ts +27 -0
  141. package/dist/gate/rules/rule-matcher.d.ts.map +1 -0
  142. package/dist/gate/rules/rule-matcher.js +228 -0
  143. package/dist/gate/rules/types.d.ts +250 -0
  144. package/dist/gate/rules/types.d.ts.map +1 -0
  145. package/dist/gate/rules/types.js +1 -0
  146. package/dist/index.d.ts +19 -0
  147. package/dist/index.d.ts.map +1 -0
  148. package/dist/index.js +35 -0
  149. package/dist/types/gate.types.d.ts +42 -0
  150. package/dist/types/gate.types.d.ts.map +1 -0
  151. package/dist/types/gate.types.js +94 -0
  152. package/dist/watch/debouncer.d.ts +90 -0
  153. package/dist/watch/debouncer.d.ts.map +1 -0
  154. package/dist/watch/debouncer.js +135 -0
  155. package/dist/watch/file-watcher.d.ts +73 -0
  156. package/dist/watch/file-watcher.d.ts.map +1 -0
  157. package/dist/watch/file-watcher.js +121 -0
  158. package/dist/watch/git-status.d.ts +98 -0
  159. package/dist/watch/git-status.d.ts.map +1 -0
  160. package/dist/watch/git-status.js +266 -0
  161. package/dist/watch/index.d.ts +16 -0
  162. package/dist/watch/index.d.ts.map +1 -0
  163. package/dist/watch/index.js +15 -0
  164. package/dist/watch/orchestrator.d.ts +113 -0
  165. package/dist/watch/orchestrator.d.ts.map +1 -0
  166. package/dist/watch/orchestrator.js +409 -0
  167. package/dist/watch/types.d.ts +190 -0
  168. package/dist/watch/types.d.ts.map +1 -0
  169. package/dist/watch/types.js +76 -0
  170. package/package.json +60 -0
@@ -0,0 +1,457 @@
1
+ /**
2
+ * Policy Check - Evaluate OPA/Rego policies against plans
3
+ */
4
+ import { execFileSync } from 'node:child_process';
5
+ import { BaseCheck } from '../check.interface.js';
6
+ import { getOPABinaryManager, PolicyLoader, OPAExecutor, } from '../policy/index.js';
7
+ import { parseSeverity, createDebugger } from '@eddacraft/anvil-core';
8
+ const log = createDebugger('check');
9
+ /**
10
+ * Default policy directory
11
+ */
12
+ const DEFAULT_POLICY_DIR = '.anvil/policies';
13
+ /**
14
+ * Score penalties per severity
15
+ */
16
+ const SEVERITY_PENALTIES = {
17
+ error: 20,
18
+ warning: 5,
19
+ info: 1,
20
+ };
21
+ /**
22
+ * Policy check that evaluates OPA/Rego policies against plans
23
+ */
24
+ export class PolicyCheck extends BaseCheck {
25
+ name = 'policy';
26
+ description = 'Evaluate OPA/Rego policies against plans';
27
+ policyLoader;
28
+ constructor() {
29
+ super();
30
+ this.policyLoader = new PolicyLoader();
31
+ }
32
+ async run(context) {
33
+ log(`policy check starting, workspace=${context.workspace_root}`);
34
+ const config = this.parseConfig(context.check_config);
35
+ // Policy check requires a plan
36
+ if (!context.plan) {
37
+ log('policy check: no plan provided, skipping');
38
+ return this.createSuccess('Policy check skipped (no plan provided)', 100, {
39
+ skipped: true,
40
+ reason: 'Policy check requires a plan',
41
+ });
42
+ }
43
+ try {
44
+ // Step 1: Ensure OPA binary is available
45
+ const binaryManager = getOPABinaryManager();
46
+ let binaryPath;
47
+ try {
48
+ binaryPath = await binaryManager.ensureBinary();
49
+ log(`policy check: OPA binary available at ${binaryPath}`);
50
+ }
51
+ catch (error) {
52
+ log(`policy check: OPA binary not available: ${error instanceof Error ? error.message : 'unknown'}`);
53
+ return this.createFailure('OPA binary not available', error instanceof Error ? error.message : 'Failed to download OPA');
54
+ }
55
+ // Step 2: Load policies
56
+ const policyDir = config.policy_dir || DEFAULT_POLICY_DIR;
57
+ log(`policy check: loading policies from ${policyDir}`);
58
+ const discoveryResult = await this.policyLoader.loadPolicies(context.workspace_root, {
59
+ policyDir,
60
+ enabledPolicies: config.enabled_policies,
61
+ disabledPolicies: config.disabled_policies,
62
+ });
63
+ // Check for policy loading errors
64
+ if (discoveryResult.errors.length > 0) {
65
+ const errorMessages = discoveryResult.errors.map((e) => `${e.path}: ${e.error}`).join('; ');
66
+ log(`policy check: policy loading errors: ${errorMessages}`);
67
+ return this.createFailure(`Failed to load some policies: ${errorMessages}`, undefined, {
68
+ loadErrors: discoveryResult.errors,
69
+ });
70
+ }
71
+ // No policies found
72
+ if (discoveryResult.policies.length === 0) {
73
+ log(`policy check: no policies configured in ${policyDir}`);
74
+ return this.createSuccess('No policies configured', 100, {
75
+ policyDir: discoveryResult.directory,
76
+ policyCount: 0,
77
+ });
78
+ }
79
+ log(`policy check: loaded ${discoveryResult.policies.length} policies`);
80
+ // Step 3: Run policy tests if required
81
+ if (config.require_policy_tests) {
82
+ log('policy check: running policy tests');
83
+ const testResult = await this.runPolicyTests(binaryPath, discoveryResult.policies, policyDir, config.timeout);
84
+ if (!testResult.passed) {
85
+ log(`policy check: policy tests failed (${testResult.failed}/${testResult.total})`);
86
+ return this.createFailure(`${testResult.failed} of ${testResult.total} policy tests failed`, testResult.details.join('; '), {
87
+ policyCount: discoveryResult.policies.length,
88
+ testResults: testResult,
89
+ });
90
+ }
91
+ }
92
+ // Step 4: Prepare OPA input
93
+ const input = this.buildOPAInput(context, config.include_git_context !== false);
94
+ // Step 4: Execute OPA
95
+ const executor = new OPAExecutor(binaryPath, {
96
+ timeout: config.timeout,
97
+ query: config.query,
98
+ includeRawOutput: false,
99
+ });
100
+ const result = await executor.evaluate(discoveryResult.policies, input);
101
+ if (!result.success) {
102
+ log(`policy check: evaluation failed: ${result.error}`);
103
+ return this.createFailure('Policy evaluation failed', result.error, {
104
+ policyCount: discoveryResult.policies.length,
105
+ executionTimeMs: result.metadata.execution_time_ms,
106
+ });
107
+ }
108
+ // Step 5: Calculate score and determine pass/fail
109
+ const { score, passed, violationsByPolicy } = this.calculateScore(result.violations, config.severity_threshold || 'error');
110
+ const message = this.buildMessage(result.violations, discoveryResult.policies, passed);
111
+ log('policy check result', {
112
+ passed,
113
+ score,
114
+ violations: result.violations.length,
115
+ policies: discoveryResult.policies.length,
116
+ executionTimeMs: result.metadata.execution_time_ms,
117
+ });
118
+ return this.createResult(passed, message, score, {
119
+ policyCount: discoveryResult.policies.length,
120
+ violationCount: result.violations.length,
121
+ violations: result.violations,
122
+ violationsByPolicy,
123
+ executionTimeMs: result.metadata.execution_time_ms,
124
+ policies: discoveryResult.policies.map((p) => ({
125
+ name: p.name,
126
+ package: p.package,
127
+ hasTests: p.hasTests,
128
+ })),
129
+ });
130
+ }
131
+ catch (error) {
132
+ log(`policy check error: ${error instanceof Error ? error.message : 'Unknown error'}`);
133
+ return this.createFailure('Policy check failed unexpectedly', error instanceof Error ? error.message : 'Unknown error');
134
+ }
135
+ }
136
+ /**
137
+ * Parse and validate check configuration
138
+ */
139
+ parseConfig(checkConfig) {
140
+ return {
141
+ policy_dir: typeof checkConfig.policy_dir === 'string' ? checkConfig.policy_dir : undefined,
142
+ severity_threshold: parseSeverity(checkConfig.severity_threshold, undefined),
143
+ enabled_policies: Array.isArray(checkConfig.enabled_policies)
144
+ ? checkConfig.enabled_policies.filter((p) => typeof p === 'string')
145
+ : undefined,
146
+ disabled_policies: Array.isArray(checkConfig.disabled_policies)
147
+ ? checkConfig.disabled_policies.filter((p) => typeof p === 'string')
148
+ : undefined,
149
+ query: typeof checkConfig.query === 'string' ? checkConfig.query : undefined,
150
+ timeout: typeof checkConfig.timeout === 'number' ? checkConfig.timeout : undefined,
151
+ require_policy_tests: typeof checkConfig.require_policy_tests === 'boolean'
152
+ ? checkConfig.require_policy_tests
153
+ : undefined,
154
+ include_git_context: typeof checkConfig.include_git_context === 'boolean'
155
+ ? checkConfig.include_git_context
156
+ : true, // Default to true
157
+ };
158
+ }
159
+ /**
160
+ * Build OPA input from check context
161
+ */
162
+ buildOPAInput(context, includeGitContext) {
163
+ const plan = context.plan; // Safe: checked in run()
164
+ // Calculate affected directories
165
+ const affectedDirectories = new Set();
166
+ for (const change of plan.proposed_changes) {
167
+ if (change.path) {
168
+ const parts = change.path.split('/');
169
+ if (parts.length > 1) {
170
+ affectedDirectories.add(parts.slice(0, -1).join('/'));
171
+ }
172
+ }
173
+ }
174
+ // Build context with optional git info
175
+ const opaContext = {
176
+ workspace_root: context.workspace_root,
177
+ timestamp: Date.now(),
178
+ };
179
+ // Add git context if enabled
180
+ if (includeGitContext) {
181
+ const gitContext = this.getGitContext(context.workspace_root);
182
+ if (gitContext) {
183
+ opaContext.git = gitContext;
184
+ }
185
+ // Add CI context from environment
186
+ const ciContext = this.getCIContext();
187
+ if (ciContext) {
188
+ opaContext.ci = ciContext;
189
+ }
190
+ }
191
+ const architecture = this.buildArchitectureInput(context);
192
+ return {
193
+ plan: {
194
+ id: plan.id,
195
+ hash: plan.hash,
196
+ intent: plan.intent,
197
+ schema_version: plan.schema_version,
198
+ proposed_changes: plan.proposed_changes.map((change) => ({
199
+ type: change.type,
200
+ path: change.path,
201
+ description: change.description,
202
+ metadata: change.metadata,
203
+ extension: change.path?.split('.').pop(),
204
+ directory: change.path?.split('/').slice(0, -1).join('/'),
205
+ })),
206
+ provenance: plan.provenance,
207
+ validations: plan.validations,
208
+ tags: plan.tags,
209
+ change_count: plan.proposed_changes.length,
210
+ affected_directories: Array.from(affectedDirectories),
211
+ },
212
+ context: opaContext,
213
+ architecture,
214
+ config: context.check_config,
215
+ };
216
+ }
217
+ /**
218
+ * Bridges ArchitectureCheck output to PolicyCheck OPA input.
219
+ * Enables Rego policies to query architecture context via input.architecture
220
+ */
221
+ buildArchitectureInput(context) {
222
+ // Cast to full ArchitectureContext - CheckContext.architectureContext is typed as base for portability
223
+ const archContext = context.architectureContext;
224
+ if (!archContext) {
225
+ return undefined;
226
+ }
227
+ const layers = {};
228
+ for (const [layerName, layerStats] of Object.entries(archContext.layers)) {
229
+ layers[layerName] = layerStats.patterns;
230
+ }
231
+ const boundaries = [];
232
+ for (const [layerName, layerStats] of Object.entries(archContext.layers)) {
233
+ for (const depLayer of layerStats.depends_on) {
234
+ boundaries.push({ from: layerName, to: depLayer });
235
+ }
236
+ }
237
+ return {
238
+ layers,
239
+ boundaries,
240
+ dependencies: archContext.dependencies,
241
+ summary: {
242
+ total_modules: archContext.summary.total_modules,
243
+ total_violations: archContext.summary.total_violations,
244
+ new_violations: archContext.summary.new_violations,
245
+ circular_count: archContext.summary.circular_count,
246
+ orphan_count: archContext.summary.orphan_count,
247
+ layer_violation_count: archContext.summary.layer_violation_count,
248
+ error_count: archContext.summary.error_count,
249
+ warn_count: archContext.summary.warn_count,
250
+ baseline_loaded: archContext.summary.baseline_loaded,
251
+ },
252
+ violations: archContext.violations.map((v) => ({
253
+ from: v.from,
254
+ to: v.to,
255
+ rule: v.rule,
256
+ severity: v.severity,
257
+ is_circular: v.is_circular,
258
+ is_new: v.is_new,
259
+ from_layer: v.from_layer,
260
+ to_layer: v.to_layer,
261
+ })),
262
+ };
263
+ }
264
+ /**
265
+ * Get git context for repository-aware policies
266
+ */
267
+ getGitContext(workspaceRoot) {
268
+ try {
269
+ const execGit = (args) => {
270
+ try {
271
+ return execFileSync('git', args, {
272
+ cwd: workspaceRoot,
273
+ encoding: 'utf-8',
274
+ stdio: ['pipe', 'pipe', 'pipe'],
275
+ }).trim();
276
+ }
277
+ catch {
278
+ return undefined;
279
+ }
280
+ };
281
+ const branch = execGit(['rev-parse', '--abbrev-ref', 'HEAD']);
282
+ if (!branch)
283
+ return undefined; // Not a git repository
284
+ const commitSha = execGit(['rev-parse', 'HEAD']);
285
+ const author = execGit(['log', '-1', '--format=%an']);
286
+ const authorEmail = execGit(['log', '-1', '--format=%ae']);
287
+ // Try to get base branch (for PRs)
288
+ let baseBranch;
289
+ // First try origin/HEAD which points to the default branch
290
+ const originHeadRef = execGit(['symbolic-ref', 'refs/remotes/origin/HEAD']);
291
+ if (originHeadRef) {
292
+ const match = originHeadRef.match(/^refs\/remotes\/origin\/(.+)$/);
293
+ if (match?.[1]) {
294
+ baseBranch = match[1];
295
+ }
296
+ }
297
+ // Fallback: probe common default branch names
298
+ if (!baseBranch) {
299
+ const defaultBranches = ['main', 'master', 'develop'];
300
+ for (const defaultBranch of defaultBranches) {
301
+ const exists = execGit(['rev-parse', '--verify', defaultBranch]);
302
+ if (exists) {
303
+ baseBranch = defaultBranch;
304
+ break;
305
+ }
306
+ }
307
+ }
308
+ return {
309
+ branch,
310
+ base_branch: baseBranch,
311
+ commit_sha: commitSha,
312
+ author,
313
+ author_email: authorEmail,
314
+ };
315
+ }
316
+ catch {
317
+ return undefined;
318
+ }
319
+ }
320
+ /**
321
+ * Get CI context from environment variables
322
+ */
323
+ getCIContext() {
324
+ // GitHub Actions
325
+ if (process.env.GITHUB_ACTIONS === 'true') {
326
+ return {
327
+ provider: 'github',
328
+ build_id: process.env.GITHUB_RUN_ID,
329
+ pr_number: process.env.GITHUB_PR_NUMBER || this.extractPRNumber(process.env.GITHUB_REF),
330
+ pr_author: process.env.GITHUB_ACTOR,
331
+ };
332
+ }
333
+ // GitLab CI
334
+ if (process.env.GITLAB_CI === 'true') {
335
+ return {
336
+ provider: 'gitlab',
337
+ build_id: process.env.CI_JOB_ID,
338
+ pr_number: process.env.CI_MERGE_REQUEST_IID,
339
+ pr_author: process.env.GITLAB_USER_LOGIN,
340
+ };
341
+ }
342
+ // Jenkins
343
+ if (process.env.JENKINS_URL) {
344
+ return {
345
+ provider: 'jenkins',
346
+ build_id: process.env.BUILD_ID,
347
+ pr_number: process.env.CHANGE_ID,
348
+ pr_author: process.env.CHANGE_AUTHOR,
349
+ };
350
+ }
351
+ // Azure DevOps
352
+ if (process.env.TF_BUILD === 'True') {
353
+ return {
354
+ provider: 'azure',
355
+ build_id: process.env.BUILD_BUILDID,
356
+ pr_number: process.env.SYSTEM_PULLREQUEST_PULLREQUESTID,
357
+ pr_author: process.env.BUILD_REQUESTEDFOR,
358
+ };
359
+ }
360
+ // Local development
361
+ return {
362
+ provider: 'local',
363
+ };
364
+ }
365
+ /**
366
+ * Extract PR number from GitHub ref (e.g., refs/pull/123/merge)
367
+ */
368
+ extractPRNumber(ref) {
369
+ if (!ref)
370
+ return undefined;
371
+ const match = ref.match(/refs\/pull\/(\d+)/);
372
+ return match?.[1];
373
+ }
374
+ /**
375
+ * Run policy tests and return results
376
+ */
377
+ async runPolicyTests(binaryPath, policies, policyDir, timeout) {
378
+ const testFiles = policies
379
+ .filter((p) => p.hasTests)
380
+ .map((p) => p.testPath)
381
+ .filter(Boolean);
382
+ if (testFiles.length === 0) {
383
+ return { passed: true, failed: 0, total: 0, details: [] };
384
+ }
385
+ const executor = new OPAExecutor(binaryPath, { timeout });
386
+ const result = await executor.runTests(policies, testFiles);
387
+ // Check for execution errors in addition to test failures
388
+ const hasErrors = result.errors.length > 0;
389
+ const errorDetails = hasErrors ? result.errors.map((e) => `Execution error: ${e}`) : [];
390
+ return {
391
+ passed: result.failed === 0 && !hasErrors,
392
+ failed: result.failed,
393
+ total: result.passed + result.failed,
394
+ details: [
395
+ ...result.details
396
+ .filter((d) => !d.passed)
397
+ .map((d) => `${d.name}: ${d.message || 'failed'}`),
398
+ ...errorDetails,
399
+ ],
400
+ };
401
+ }
402
+ /**
403
+ * Calculate score based on violations
404
+ */
405
+ calculateScore(violations, severityThreshold) {
406
+ // Group violations by policy
407
+ const violationsByPolicy = {};
408
+ for (const v of violations) {
409
+ const policy = v.policy || 'unknown';
410
+ if (!violationsByPolicy[policy]) {
411
+ violationsByPolicy[policy] = [];
412
+ }
413
+ violationsByPolicy[policy].push(v);
414
+ }
415
+ // Calculate total penalty
416
+ let totalPenalty = 0;
417
+ let hasBlockingViolation = false;
418
+ for (const v of violations) {
419
+ const penalty = SEVERITY_PENALTIES[v.severity] || 0;
420
+ totalPenalty += penalty;
421
+ // Check if this violation should block
422
+ if (this.isBlockingSeverity(v.severity, severityThreshold)) {
423
+ hasBlockingViolation = true;
424
+ }
425
+ }
426
+ const score = Math.max(0, 100 - totalPenalty);
427
+ const passed = !hasBlockingViolation;
428
+ return { score, passed, violationsByPolicy };
429
+ }
430
+ /**
431
+ * Check if a severity level should block the check
432
+ */
433
+ isBlockingSeverity(severity, threshold) {
434
+ const levels = { error: 3, warning: 2, info: 1 };
435
+ return levels[severity] >= levels[threshold];
436
+ }
437
+ /**
438
+ * Build human-readable message
439
+ */
440
+ buildMessage(violations, policies, passed) {
441
+ if (violations.length === 0) {
442
+ return `All ${policies.length} policies passed`;
443
+ }
444
+ const errorCount = violations.filter((v) => v.severity === 'error').length;
445
+ const warningCount = violations.filter((v) => v.severity === 'warning').length;
446
+ const infoCount = violations.filter((v) => v.severity === 'info').length;
447
+ const parts = [];
448
+ if (errorCount > 0)
449
+ parts.push(`${errorCount} error${errorCount > 1 ? 's' : ''}`);
450
+ if (warningCount > 0)
451
+ parts.push(`${warningCount} warning${warningCount > 1 ? 's' : ''}`);
452
+ if (infoCount > 0)
453
+ parts.push(`${infoCount} info`);
454
+ const status = passed ? 'passed with issues' : 'failed';
455
+ return `Policy check ${status}: ${parts.join(', ')}`;
456
+ }
457
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Entropy Detector - Shannon entropy-based detection for secrets
3
+ *
4
+ * Detects high-entropy strings that are likely to be secrets or sensitive data.
5
+ */
6
+ /**
7
+ * Secret finding from entropy detection
8
+ */
9
+ export interface EntropyFinding {
10
+ file: string;
11
+ line: number;
12
+ type: string;
13
+ match: string;
14
+ context: string;
15
+ entropy: number;
16
+ source: 'entropy';
17
+ }
18
+ /**
19
+ * Configuration for entropy detection
20
+ */
21
+ export interface EntropyDetectorConfig {
22
+ /** Minimum entropy threshold for detection (default: 4.5) */
23
+ entropy_threshold: number;
24
+ /** Minimum string length for entropy analysis (default: 16) */
25
+ min_entropy_length: number;
26
+ /** Patterns to allowlist (reduce false positives) */
27
+ allowlist: string[];
28
+ }
29
+ /**
30
+ * Entropy detector for high-entropy strings
31
+ */
32
+ export declare class EntropyDetector {
33
+ private matcher;
34
+ /**
35
+ * Calculate Shannon entropy of a string
36
+ * Higher entropy = more randomness = likely a secret
37
+ */
38
+ calculateEntropy(str: string): number;
39
+ /**
40
+ * Detect high-entropy strings that might be secrets
41
+ */
42
+ detectHighEntropyStrings(line: string, lineNumber: number, file: string, config: EntropyDetectorConfig): EntropyFinding[];
43
+ }
44
+ //# sourceMappingURL=entropy-detector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entropy-detector.d.ts","sourceRoot":"","sources":["../../../../src/gate/checks/secret/entropy-detector.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,6DAA6D;IAC7D,iBAAiB,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,qDAAqD;IACrD,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,OAAO,CAAwB;IAEvC;;;OAGG;IACH,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAkBrC;;OAEG;IACH,wBAAwB,CACtB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,qBAAqB,GAC5B,cAAc,EAAE;CA6CpB"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Entropy Detector - Shannon entropy-based detection for secrets
3
+ *
4
+ * Detects high-entropy strings that are likely to be secrets or sensitive data.
5
+ */
6
+ import { PatternMatcher } from './secret-patterns.js';
7
+ import { createDebugger } from '@eddacraft/anvil-core';
8
+ const log = createDebugger('check');
9
+ /**
10
+ * Entropy detector for high-entropy strings
11
+ */
12
+ export class EntropyDetector {
13
+ matcher = new PatternMatcher();
14
+ /**
15
+ * Calculate Shannon entropy of a string
16
+ * Higher entropy = more randomness = likely a secret
17
+ */
18
+ calculateEntropy(str) {
19
+ if (!str || str.length === 0)
20
+ return 0;
21
+ const charFrequency = {};
22
+ for (const char of str) {
23
+ charFrequency[char] = (charFrequency[char] || 0) + 1;
24
+ }
25
+ let entropy = 0;
26
+ const len = str.length;
27
+ for (const char in charFrequency) {
28
+ const frequency = charFrequency[char] / len;
29
+ entropy -= frequency * Math.log2(frequency);
30
+ }
31
+ return entropy;
32
+ }
33
+ /**
34
+ * Detect high-entropy strings that might be secrets
35
+ */
36
+ detectHighEntropyStrings(line, lineNumber, file, config) {
37
+ const findings = [];
38
+ const threshold = config.entropy_threshold;
39
+ const minLength = config.min_entropy_length;
40
+ // Extract potential secret strings (quoted strings, assignments)
41
+ const stringPatterns = [
42
+ // Quoted strings
43
+ /['"]([^'"]{16,})['"]/,
44
+ // Variable assignments with alphanumeric values
45
+ /[:=]\s*['"]?([a-zA-Z0-9_/+=-]{16,})['"]?/,
46
+ ];
47
+ for (const pattern of stringPatterns) {
48
+ const matches = line.match(pattern);
49
+ if (matches && matches[1]) {
50
+ const candidate = matches[1];
51
+ // Skip if too short or already detected by pattern
52
+ if (candidate.length < minLength)
53
+ continue;
54
+ if (this.matcher.isAllowlisted(candidate, config.allowlist))
55
+ continue;
56
+ // Skip if it looks like code or common patterns
57
+ if (this.matcher.looksLikeCode(candidate))
58
+ continue;
59
+ const entropy = this.calculateEntropy(candidate);
60
+ if (entropy >= threshold) {
61
+ log(`entropy-detector: high entropy string found in ${file}:${lineNumber} (entropy=${entropy.toFixed(2)}, threshold=${threshold})`);
62
+ findings.push({
63
+ file,
64
+ line: lineNumber,
65
+ type: 'High Entropy String',
66
+ match: this.matcher.redactSecret(candidate),
67
+ context: this.matcher.redactLine(line.trim()),
68
+ entropy: Math.round(entropy * 100) / 100,
69
+ source: 'entropy',
70
+ });
71
+ }
72
+ }
73
+ }
74
+ return findings;
75
+ }
76
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Git Scanner - Scan git history for secrets in recent commits
3
+ *
4
+ * Scans git commit diffs to find secrets that may have been committed in the past.
5
+ */
6
+ /**
7
+ * Secret finding from git history
8
+ */
9
+ export interface GitHistoryFinding {
10
+ file: string;
11
+ line: number;
12
+ type: string;
13
+ match: string;
14
+ context: string;
15
+ source: 'git-history';
16
+ }
17
+ /**
18
+ * Configuration for git scanning
19
+ */
20
+ export interface GitScannerConfig {
21
+ /** Number of commits to scan in git history (default: 10) */
22
+ git_history_depth: number;
23
+ /** Patterns to allowlist (reduce false positives) */
24
+ allowlist: string[];
25
+ }
26
+ /**
27
+ * Git scanner for finding secrets in repository history
28
+ */
29
+ export declare class GitScanner {
30
+ private matcher;
31
+ /**
32
+ * Scan git history for secrets in recent commits
33
+ */
34
+ scanGitHistory(workspaceRoot: string, config: GitScannerConfig): Promise<GitHistoryFinding[]>;
35
+ }
36
+ //# sourceMappingURL=git-scanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-scanner.d.ts","sourceRoot":"","sources":["../../../../src/gate/checks/secret/git-scanner.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,6DAA6D;IAC7D,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,OAAO,CAAwB;IAEvC;;OAEG;IACG,cAAc,CAClB,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,iBAAiB,EAAE,CAAC;CAkFhC"}