@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.
- package/LICENSE +14 -0
- package/dist/cache/cache-key.d.ts +45 -0
- package/dist/cache/cache-key.d.ts.map +1 -0
- package/dist/cache/cache-key.js +135 -0
- package/dist/cache/index.d.ts +27 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +38 -0
- package/dist/cache/providers/file-cache.d.ts +63 -0
- package/dist/cache/providers/file-cache.d.ts.map +1 -0
- package/dist/cache/providers/file-cache.js +369 -0
- package/dist/cache/providers/memory-cache.d.ts +52 -0
- package/dist/cache/providers/memory-cache.d.ts.map +1 -0
- package/dist/cache/providers/memory-cache.js +197 -0
- package/dist/cache/providers/null-cache.d.ts +26 -0
- package/dist/cache/providers/null-cache.d.ts.map +1 -0
- package/dist/cache/providers/null-cache.js +50 -0
- package/dist/cache/types.d.ts +114 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cache/types.js +4 -0
- package/dist/concurrency/agent.d.ts +137 -0
- package/dist/concurrency/agent.d.ts.map +1 -0
- package/dist/concurrency/agent.js +440 -0
- package/dist/concurrency/atomic.d.ts +93 -0
- package/dist/concurrency/atomic.d.ts.map +1 -0
- package/dist/concurrency/atomic.js +281 -0
- package/dist/concurrency/git-agent.d.ts +114 -0
- package/dist/concurrency/git-agent.d.ts.map +1 -0
- package/dist/concurrency/git-agent.js +313 -0
- package/dist/concurrency/index.d.ts +95 -0
- package/dist/concurrency/index.d.ts.map +1 -0
- package/dist/concurrency/index.js +127 -0
- package/dist/concurrency/lock-manager.d.ts +170 -0
- package/dist/concurrency/lock-manager.d.ts.map +1 -0
- package/dist/concurrency/lock-manager.js +525 -0
- package/dist/concurrency/queue-manager.d.ts +166 -0
- package/dist/concurrency/queue-manager.d.ts.map +1 -0
- package/dist/concurrency/queue-manager.js +442 -0
- package/dist/concurrency/types.d.ts +382 -0
- package/dist/concurrency/types.d.ts.map +1 -0
- package/dist/concurrency/types.js +204 -0
- package/dist/export/constraint-collector.d.ts +175 -0
- package/dist/export/constraint-collector.d.ts.map +1 -0
- package/dist/export/constraint-collector.js +203 -0
- package/dist/export/formatters/llms-txt-formatter.d.ts +89 -0
- package/dist/export/formatters/llms-txt-formatter.d.ts.map +1 -0
- package/dist/export/formatters/llms-txt-formatter.js +249 -0
- package/dist/export/formatters/mcp-resource-formatter.d.ts +186 -0
- package/dist/export/formatters/mcp-resource-formatter.d.ts.map +1 -0
- package/dist/export/formatters/mcp-resource-formatter.js +139 -0
- package/dist/export/formatters/prompt-formatter.d.ts +83 -0
- package/dist/export/formatters/prompt-formatter.d.ts.map +1 -0
- package/dist/export/formatters/prompt-formatter.js +256 -0
- package/dist/export/index.d.ts +10 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/export/index.js +9 -0
- package/dist/gate/check.interface.d.ts +15 -0
- package/dist/gate/check.interface.d.ts.map +1 -0
- package/dist/gate/check.interface.js +18 -0
- package/dist/gate/checks/antipattern.check.d.ts +27 -0
- package/dist/gate/checks/antipattern.check.d.ts.map +1 -0
- package/dist/gate/checks/antipattern.check.js +140 -0
- package/dist/gate/checks/architecture/circular-detector.d.ts +33 -0
- package/dist/gate/checks/architecture/circular-detector.d.ts.map +1 -0
- package/dist/gate/checks/architecture/circular-detector.js +71 -0
- package/dist/gate/checks/architecture/dependency-analyzer.d.ts +81 -0
- package/dist/gate/checks/architecture/dependency-analyzer.d.ts.map +1 -0
- package/dist/gate/checks/architecture/dependency-analyzer.js +136 -0
- package/dist/gate/checks/architecture/layer-validator.d.ts +75 -0
- package/dist/gate/checks/architecture/layer-validator.d.ts.map +1 -0
- package/dist/gate/checks/architecture/layer-validator.js +193 -0
- package/dist/gate/checks/architecture.check.d.ts +56 -0
- package/dist/gate/checks/architecture.check.d.ts.map +1 -0
- package/dist/gate/checks/architecture.check.js +394 -0
- package/dist/gate/checks/command-safety.check.d.ts +12 -0
- package/dist/gate/checks/command-safety.check.d.ts.map +1 -0
- package/dist/gate/checks/command-safety.check.js +230 -0
- package/dist/gate/checks/coverage.check.d.ts +9 -0
- package/dist/gate/checks/coverage.check.d.ts.map +1 -0
- package/dist/gate/checks/coverage.check.js +81 -0
- package/dist/gate/checks/dependency.check.d.ts +17 -0
- package/dist/gate/checks/dependency.check.d.ts.map +1 -0
- package/dist/gate/checks/dependency.check.js +342 -0
- package/dist/gate/checks/eslint.check.d.ts +14 -0
- package/dist/gate/checks/eslint.check.d.ts.map +1 -0
- package/dist/gate/checks/eslint.check.js +79 -0
- package/dist/gate/checks/policy.check.d.ts +78 -0
- package/dist/gate/checks/policy.check.d.ts.map +1 -0
- package/dist/gate/checks/policy.check.js +457 -0
- package/dist/gate/checks/secret/entropy-detector.d.ts +44 -0
- package/dist/gate/checks/secret/entropy-detector.d.ts.map +1 -0
- package/dist/gate/checks/secret/entropy-detector.js +76 -0
- package/dist/gate/checks/secret/git-scanner.d.ts +36 -0
- package/dist/gate/checks/secret/git-scanner.d.ts.map +1 -0
- package/dist/gate/checks/secret/git-scanner.js +90 -0
- package/dist/gate/checks/secret/secret-patterns.d.ts +42 -0
- package/dist/gate/checks/secret/secret-patterns.d.ts.map +1 -0
- package/dist/gate/checks/secret/secret-patterns.js +137 -0
- package/dist/gate/checks/secret.check.d.ts +56 -0
- package/dist/gate/checks/secret.check.d.ts.map +1 -0
- package/dist/gate/checks/secret.check.js +245 -0
- package/dist/gate/config/command-safety-config.d.ts +5 -0
- package/dist/gate/config/command-safety-config.d.ts.map +1 -0
- package/dist/gate/config/command-safety-config.js +69 -0
- package/dist/gate/config/index.d.ts +2 -0
- package/dist/gate/config/index.d.ts.map +1 -0
- package/dist/gate/config/index.js +1 -0
- package/dist/gate/formatters/command-safety-formatter.d.ts +10 -0
- package/dist/gate/formatters/command-safety-formatter.d.ts.map +1 -0
- package/dist/gate/formatters/command-safety-formatter.js +64 -0
- package/dist/gate/formatters/index.d.ts +2 -0
- package/dist/gate/formatters/index.d.ts.map +1 -0
- package/dist/gate/formatters/index.js +1 -0
- package/dist/gate/gate-config.d.ts +44 -0
- package/dist/gate/gate-config.d.ts.map +1 -0
- package/dist/gate/gate-config.js +334 -0
- package/dist/gate/gate-runner.d.ts +160 -0
- package/dist/gate/gate-runner.d.ts.map +1 -0
- package/dist/gate/gate-runner.js +531 -0
- package/dist/gate/index.d.ts +20 -0
- package/dist/gate/index.d.ts.map +1 -0
- package/dist/gate/index.js +14 -0
- package/dist/gate/parsers/command-parser.d.ts +18 -0
- package/dist/gate/parsers/command-parser.d.ts.map +1 -0
- package/dist/gate/parsers/command-parser.js +363 -0
- package/dist/gate/parsers/index.d.ts +2 -0
- package/dist/gate/parsers/index.d.ts.map +1 -0
- package/dist/gate/parsers/index.js +1 -0
- package/dist/gate/policy/index.d.ts +12 -0
- package/dist/gate/policy/index.d.ts.map +1 -0
- package/dist/gate/policy/index.js +10 -0
- package/dist/gate/rules/default-filesystem-rules.d.ts +3 -0
- package/dist/gate/rules/default-filesystem-rules.d.ts.map +1 -0
- package/dist/gate/rules/default-filesystem-rules.js +201 -0
- package/dist/gate/rules/default-git-rules.d.ts +3 -0
- package/dist/gate/rules/default-git-rules.d.ts.map +1 -0
- package/dist/gate/rules/default-git-rules.js +192 -0
- package/dist/gate/rules/index.d.ts +5 -0
- package/dist/gate/rules/index.d.ts.map +1 -0
- package/dist/gate/rules/index.js +3 -0
- package/dist/gate/rules/rule-matcher.d.ts +27 -0
- package/dist/gate/rules/rule-matcher.d.ts.map +1 -0
- package/dist/gate/rules/rule-matcher.js +228 -0
- package/dist/gate/rules/types.d.ts +250 -0
- package/dist/gate/rules/types.d.ts.map +1 -0
- package/dist/gate/rules/types.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/types/gate.types.d.ts +42 -0
- package/dist/types/gate.types.d.ts.map +1 -0
- package/dist/types/gate.types.js +94 -0
- package/dist/watch/debouncer.d.ts +90 -0
- package/dist/watch/debouncer.d.ts.map +1 -0
- package/dist/watch/debouncer.js +135 -0
- package/dist/watch/file-watcher.d.ts +73 -0
- package/dist/watch/file-watcher.d.ts.map +1 -0
- package/dist/watch/file-watcher.js +121 -0
- package/dist/watch/git-status.d.ts +98 -0
- package/dist/watch/git-status.d.ts.map +1 -0
- package/dist/watch/git-status.js +266 -0
- package/dist/watch/index.d.ts +16 -0
- package/dist/watch/index.d.ts.map +1 -0
- package/dist/watch/index.js +15 -0
- package/dist/watch/orchestrator.d.ts +113 -0
- package/dist/watch/orchestrator.d.ts.map +1 -0
- package/dist/watch/orchestrator.js +409 -0
- package/dist/watch/types.d.ts +190 -0
- package/dist/watch/types.d.ts.map +1 -0
- package/dist/watch/types.js +76 -0
- 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"}
|