@saiteja1123/mcp-server 1.1.4 → 1.1.6

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 (37) hide show
  1. package/package.json +59 -55
  2. package/src/api-scan.mjs +362 -93
  3. package/src/cli.js +771 -322
  4. package/src/deep-scan/contracts.js +201 -0
  5. package/src/deep-scan/deterministic-scan.js +337 -0
  6. package/src/deep-scan/index.js +109 -0
  7. package/src/deep-scan/project-map.js +507 -0
  8. package/src/deep-scan/ralph-accept.js +510 -0
  9. package/src/deep-scan/ralph-compare.js +498 -0
  10. package/src/deep-scan/ralph-tasks.js +598 -0
  11. package/src/deep-scan/ralph-track.js +548 -0
  12. package/src/deep-scan/registry.js +159 -0
  13. package/src/deep-scan/runtime.js +275 -0
  14. package/src/deep-scan/sample-steppers.js +128 -0
  15. package/src/deep-scan/sourceSafe.js +73 -0
  16. package/src/deep-scan/status.js +70 -0
  17. package/src/deep-scan/store.js +57 -0
  18. package/src/deep-scan/test-plan.js +760 -0
  19. package/src/index.js +6 -5
  20. package/src/lock.mjs +55 -14
  21. package/src/mcp-config.mjs +161 -0
  22. package/src/middleware/governance.js +135 -0
  23. package/src/orchestrator/runScan.js +211 -0
  24. package/src/project-bindings.mjs +215 -0
  25. package/src/rule-engine/index.js +2 -1
  26. package/src/rule-engine/localScan.js +39 -12
  27. package/src/rule-engine/metadata.js +20 -0
  28. package/src/rule-engine/prompt.js +6 -5
  29. package/src/rule-engine/rules.js +71 -43
  30. package/src/rule-engine/score.js +5 -4
  31. package/src/security/pathGuard.js +170 -0
  32. package/src/selftest.js +2473 -0
  33. package/src/server.js +109 -150
  34. package/src/tools/deepScan.js +286 -0
  35. package/src/tools/localScan.js +85 -0
  36. package/src/tools/projects.js +124 -0
  37. package/src/tools/scanFile.js +131 -0
@@ -0,0 +1,201 @@
1
+ import { assertSourceSafePayload, cloneSourceSafe } from './sourceSafe.js';
2
+
3
+ export const STEP_STATUSES = Object.freeze([
4
+ 'pending',
5
+ 'running',
6
+ 'passed',
7
+ 'failed',
8
+ 'skipped',
9
+ 'blocked',
10
+ 'needs_human',
11
+ ]);
12
+
13
+ export const APPROVAL_DECISIONS = Object.freeze(['requested', 'approved', 'denied']);
14
+
15
+ const UUIDISH = /^[a-zA-Z0-9_.:-]+$/;
16
+ const SHA256 = /^[a-f0-9]{64}$/i;
17
+
18
+ const requireString = (value, name, { max = 500, pattern = null } = {}) => {
19
+ if (typeof value !== 'string' || !value.trim()) {
20
+ throw new Error(`${name} must be a non-empty string`);
21
+ }
22
+ if (value.length > max) throw new Error(`${name} must be ${max} characters or fewer`);
23
+ if (pattern && !pattern.test(value)) throw new Error(`${name} has an invalid format`);
24
+ return value;
25
+ };
26
+
27
+ const optionalString = (value, name, options) => {
28
+ if (value === undefined || value === null) return undefined;
29
+ return requireString(value, name, options);
30
+ };
31
+
32
+ const requireIsoDate = (value, name) => {
33
+ requireString(value, name, { max: 40 });
34
+ if (Number.isNaN(Date.parse(value))) throw new Error(`${name} must be an ISO timestamp`);
35
+ return value;
36
+ };
37
+
38
+ export const validateRunIdSegment = (value, name = 'runId') => {
39
+ const runId = requireString(value, name, { max: 160, pattern: UUIDISH });
40
+ if (runId === '.' || runId === '..') throw new Error(`${name} must not be a dot-only path segment`);
41
+ return runId;
42
+ };
43
+
44
+ const asArray = (value, name) => {
45
+ if (value === undefined) return [];
46
+ if (!Array.isArray(value)) throw new Error(`${name} must be an array`);
47
+ return value;
48
+ };
49
+
50
+ export function createArtifactRef(input) {
51
+ assertSourceSafePayload(input, 'artifact');
52
+ const artifact = {
53
+ id: requireString(input?.id, 'artifact.id', { max: 120, pattern: UUIDISH }),
54
+ type: requireString(input?.type, 'artifact.type', { max: 80, pattern: UUIDISH }),
55
+ storage: requireString(input?.storage, 'artifact.storage', { max: 40, pattern: UUIDISH }),
56
+ uri: optionalString(input?.uri, 'artifact.uri', { max: 1000 }),
57
+ hash: optionalString(input?.hash, 'artifact.hash', { max: 64, pattern: SHA256 }),
58
+ preview: optionalString(input?.preview, 'artifact.preview', { max: 500 }),
59
+ metadata: cloneSourceSafe(input?.metadata || {}, 'artifact.metadata'),
60
+ };
61
+ assertSourceSafePayload(artifact, 'artifact');
62
+ return artifact;
63
+ }
64
+
65
+ export function validateArtifactRef(input) {
66
+ return createArtifactRef(input);
67
+ }
68
+
69
+ export function createReceiptRef(input = {}) {
70
+ assertSourceSafePayload(input, 'receipt');
71
+ const receipt = {
72
+ id: requireString(input.id, 'receipt.id', { max: 160, pattern: UUIDISH }),
73
+ type: requireString(input.type, 'receipt.type', { max: 80, pattern: UUIDISH }),
74
+ source: requireString(input.source, 'receipt.source', { max: 80, pattern: UUIDISH }),
75
+ artifactId: optionalString(input.artifactId, 'receipt.artifactId', { max: 120, pattern: UUIDISH }),
76
+ artifactHash: optionalString(input.artifactHash, 'receipt.artifactHash', { max: 64, pattern: SHA256 }),
77
+ engine: cloneSourceSafe(input.engine || {}, 'receipt.engine'),
78
+ summary: cloneSourceSafe(input.summary || {}, 'receipt.summary'),
79
+ verification: cloneSourceSafe(input.verification || {}, 'receipt.verification'),
80
+ metadata: cloneSourceSafe(input.metadata || {}, 'receipt.metadata'),
81
+ createdAt: input.createdAt ? requireIsoDate(input.createdAt, 'receipt.createdAt') : new Date().toISOString(),
82
+ };
83
+ assertSourceSafePayload(receipt, 'receipt');
84
+ return receipt;
85
+ }
86
+
87
+ export function validateReceiptRef(input) {
88
+ return createReceiptRef(input);
89
+ }
90
+
91
+ export function validateEvidence(input = {}) {
92
+ assertSourceSafePayload(input, 'evidence');
93
+ const evidence = {
94
+ type: requireString(input.type, 'evidence.type', { max: 80, pattern: UUIDISH }),
95
+ label: requireString(input.label, 'evidence.label', { max: 160 }),
96
+ uri: optionalString(input.uri, 'evidence.uri', { max: 1000 }),
97
+ hash: optionalString(input.hash, 'evidence.hash', { max: 64, pattern: SHA256 }),
98
+ preview: optionalString(input.preview, 'evidence.preview', { max: 500 }),
99
+ summary: optionalString(input.summary, 'evidence.summary', { max: 1000 }),
100
+ metadata: cloneSourceSafe(input.metadata || {}, 'evidence.metadata'),
101
+ };
102
+ assertSourceSafePayload(evidence, 'evidence');
103
+ return evidence;
104
+ }
105
+
106
+ export function validateFinding(input = {}) {
107
+ assertSourceSafePayload(input, 'finding');
108
+ const finding = {
109
+ id: optionalString(input.id, 'finding.id', { max: 120, pattern: UUIDISH }),
110
+ ruleId: optionalString(input.ruleId, 'finding.ruleId', { max: 80, pattern: UUIDISH }),
111
+ ruleName: optionalString(input.ruleName, 'finding.ruleName', { max: 160 }),
112
+ severity: optionalString(input.severity, 'finding.severity', { max: 20, pattern: UUIDISH }),
113
+ category: optionalString(input.category, 'finding.category', { max: 80, pattern: UUIDISH }),
114
+ title: optionalString(input.title, 'finding.title', { max: 200 }),
115
+ status: optionalString(input.status, 'finding.status', { max: 40, pattern: UUIDISH }),
116
+ source: cloneSourceSafe(input.source || {}, 'finding.source'),
117
+ fingerprint: optionalString(input.fingerprint, 'finding.fingerprint', { max: 64, pattern: SHA256 }),
118
+ snippetPreview: optionalString(input.snippetPreview, 'finding.snippetPreview', { max: 200 }),
119
+ fix: optionalString(input.fix, 'finding.fix', { max: 500 }),
120
+ evidence: asArray(input.evidence, 'finding.evidence').map(validateEvidence),
121
+ metadata: cloneSourceSafe(input.metadata || {}, 'finding.metadata'),
122
+ };
123
+ assertSourceSafePayload(finding, 'finding');
124
+ return finding;
125
+ }
126
+
127
+ export function validateApproval(input = {}) {
128
+ assertSourceSafePayload(input, 'approval');
129
+ const approval = {
130
+ id: optionalString(input.id, 'approval.id', { max: 120, pattern: UUIDISH }),
131
+ requirementId: requireString(input.requirementId, 'approval.requirementId', { max: 160 }),
132
+ decision: requireString(input.decision, 'approval.decision', { max: 20, pattern: UUIDISH }),
133
+ approvedBy: optionalString(input.approvedBy, 'approval.approvedBy', { max: 255 }),
134
+ reason: optionalString(input.reason, 'approval.reason', { max: 1000 }),
135
+ relatedFindingId: optionalString(input.relatedFindingId, 'approval.relatedFindingId', { max: 120 }),
136
+ relatedArtifactId: optionalString(input.relatedArtifactId, 'approval.relatedArtifactId', { max: 120 }),
137
+ metadata: cloneSourceSafe(input.metadata || {}, 'approval.metadata'),
138
+ createdAt: input.createdAt ? requireIsoDate(input.createdAt, 'approval.createdAt') : new Date().toISOString(),
139
+ };
140
+ if (!APPROVAL_DECISIONS.includes(approval.decision)) {
141
+ throw new Error(`approval.decision must be one of ${APPROVAL_DECISIONS.join(', ')}`);
142
+ }
143
+ if (approval.decision !== 'requested' && !approval.reason) {
144
+ throw new Error('approval.reason is required for approved or denied decisions');
145
+ }
146
+ assertSourceSafePayload(approval, 'approval');
147
+ return approval;
148
+ }
149
+
150
+ export function validateStepResult(input = {}, expected = {}) {
151
+ assertSourceSafePayload(input, 'stepResult');
152
+ const stepperId = requireString(input.stepperId, 'stepResult.stepperId', { max: 160 });
153
+ const version = requireString(input.version, 'stepResult.version', { max: 80 });
154
+ if (expected.stepperId && stepperId !== expected.stepperId) {
155
+ throw new Error(`stepResult.stepperId must match ${expected.stepperId}`);
156
+ }
157
+ if (expected.version && version !== expected.version) {
158
+ throw new Error(`stepResult.version must match ${expected.version}`);
159
+ }
160
+ const status = requireString(input.status, 'stepResult.status', { max: 40, pattern: UUIDISH });
161
+ if (!STEP_STATUSES.includes(status)) {
162
+ throw new Error(`stepResult.status must be one of ${STEP_STATUSES.join(', ')}`);
163
+ }
164
+ const result = {
165
+ stepperId,
166
+ version,
167
+ status,
168
+ startedAt: requireIsoDate(input.startedAt, 'stepResult.startedAt'),
169
+ finishedAt: requireIsoDate(input.finishedAt, 'stepResult.finishedAt'),
170
+ evidence: asArray(input.evidence, 'stepResult.evidence').map(validateEvidence),
171
+ findings: asArray(input.findings, 'stepResult.findings').map(validateFinding),
172
+ artifacts: asArray(input.artifacts, 'stepResult.artifacts').map(validateArtifactRef),
173
+ receipts: asArray(input.receipts, 'stepResult.receipts').map(validateReceiptRef),
174
+ summary: requireString(input.summary, 'stepResult.summary', { max: 1000 }),
175
+ nextActions: asArray(input.nextActions, 'stepResult.nextActions').map((item, index) =>
176
+ requireString(item, `stepResult.nextActions[${index}]`, { max: 300 })),
177
+ skippedReason: optionalString(input.skippedReason, 'stepResult.skippedReason', { max: 500 }),
178
+ blockedReason: optionalString(input.blockedReason, 'stepResult.blockedReason', { max: 500 }),
179
+ needsHumanReason: optionalString(input.needsHumanReason, 'stepResult.needsHumanReason', { max: 500 }),
180
+ };
181
+ if (status === 'skipped' && !result.skippedReason) throw new Error('skipped steps require skippedReason');
182
+ if (status === 'blocked' && !result.blockedReason) throw new Error('blocked steps require blockedReason');
183
+ if (status === 'needs_human' && !result.needsHumanReason) {
184
+ throw new Error('needs_human steps require needsHumanReason');
185
+ }
186
+ assertSourceSafePayload(result, 'stepResult');
187
+ return result;
188
+ }
189
+
190
+ export function validateDeepScanState(input = {}) {
191
+ const state = cloneSourceSafe(input, 'deepScanState');
192
+ validateRunIdSegment(state.runId, 'deepScanState.runId');
193
+ requireString(state.projectId, 'deepScanState.projectId', { max: 160 });
194
+ requireString(state.graphProfileId, 'deepScanState.graphProfileId', { max: 160 });
195
+ if (!STEP_STATUSES.includes(state.status)) {
196
+ throw new Error(`deepScanState.status must be one of ${STEP_STATUSES.join(', ')}`);
197
+ }
198
+ requireIsoDate(state.createdAt, 'deepScanState.createdAt');
199
+ requireIsoDate(state.updatedAt, 'deepScanState.updatedAt');
200
+ return state;
201
+ }
@@ -0,0 +1,337 @@
1
+ import crypto from 'crypto';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import {
5
+ DEFAULT_EXCLUDE,
6
+ DEFAULT_INCLUDE,
7
+ gatherRepoScan,
8
+ } from '../repo-scan.mjs';
9
+ import { getGrade, getRuleEngineMetadata } from '../rule-engine/index.js';
10
+ import { createArtifactRef, createReceiptRef, validateRunIdSegment } from './contracts.js';
11
+ import { assertSourceSafePayload, cloneSourceSafe, findRawSourceField } from './sourceSafe.js';
12
+
13
+ export const DETERMINISTIC_SCAN_SCHEMA_VERSION = 'deterministic_scan.v1';
14
+ export const DETERMINISTIC_SCAN_STEPPER_ID = 'rules.scan';
15
+ export const DETERMINISTIC_SCAN_STEPPER_VERSION = '1.0.0';
16
+
17
+ const DEFAULT_MAX_FILES = 300;
18
+ const LOCAL_ASSURANCE_EXCLUDE = ['**/.vibesecur/**'];
19
+ const SECRETISH_PATTERNS = [
20
+ /\b(?:sk|pk)_(?:live|test)_[a-zA-Z0-9_=-]{6,}\b/gi,
21
+ /\bgh[pousr]_[a-zA-Z0-9_]{10,}\b/g,
22
+ /\bAKIA[0-9A-Z]{12,}\b/g,
23
+ /\bxox[baprs]-[a-zA-Z0-9-]{10,}\b/gi,
24
+ /-----BEGIN [A-Z ]*(?:PRIVATE KEY|SECRET|TOKEN|CERTIFICATE)-----[\s\S]*?-----END [A-Z ]*-----/gi,
25
+ ];
26
+ const nowIso = () => new Date().toISOString();
27
+ const toPosix = (value) => String(value || '').replace(/\\/g, '/');
28
+
29
+ function stableSort(value) {
30
+ if (Array.isArray(value)) return value.map(stableSort);
31
+ if (!value || typeof value !== 'object') return value;
32
+ return Object.keys(value)
33
+ .sort()
34
+ .reduce((acc, key) => {
35
+ acc[key] = stableSort(value[key]);
36
+ return acc;
37
+ }, {});
38
+ }
39
+
40
+ function stableStringify(value) {
41
+ return JSON.stringify(stableSort(value));
42
+ }
43
+
44
+ function sha256(value) {
45
+ return crypto.createHash('sha256').update(value).digest('hex');
46
+ }
47
+
48
+ function redactString(value) {
49
+ if (typeof value !== 'string') return value;
50
+ let redacted = value;
51
+ for (const pattern of SECRETISH_PATTERNS) redacted = redacted.replace(pattern, '[redacted]');
52
+ redacted = redacted.replace(
53
+ /\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY|PRIVATE_KEY)[A-Z0-9_]*)=("[^"]*"|'[^']*'|\S+)/gi,
54
+ '$1=[redacted]',
55
+ );
56
+ return redacted;
57
+ }
58
+
59
+ function safeText(value, fallback, max = 200) {
60
+ const redacted = redactString(String(value || '').replace(/\s+/g, ' ').trim()).slice(0, max);
61
+ if (!redacted) return fallback;
62
+ if (findRawSourceField(redacted, 'preview')) return fallback;
63
+ return redacted;
64
+ }
65
+
66
+ function safeToken(value, fallback, max = 80) {
67
+ const text = safeText(value, fallback, max)
68
+ .toLowerCase()
69
+ .replace(/[^a-z0-9_.:-]+/g, '_')
70
+ .replace(/^_+|_+$/g, '')
71
+ .slice(0, max);
72
+ return text || fallback;
73
+ }
74
+
75
+ function safeSnippetPreview(_value, fallback) {
76
+ return fallback;
77
+ }
78
+
79
+ function safeRelativePath(rootPath, filePath) {
80
+ const relative = path.relative(rootPath, filePath);
81
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return path.basename(filePath);
82
+ return toPosix(relative);
83
+ }
84
+
85
+ function findingCounts(findings) {
86
+ return findings.reduce((acc, finding) => {
87
+ acc[finding.severity] = (acc[finding.severity] || 0) + 1;
88
+ return acc;
89
+ }, { critical: 0, high: 0, medium: 0, low: 0 });
90
+ }
91
+
92
+ function normalizeFinding({ rootPath, filePath, finding }) {
93
+ const relativePath = safeRelativePath(rootPath, filePath);
94
+ const lineNumber = Number.isInteger(finding.lineNumber) ? finding.lineNumber : null;
95
+ const endLineNumber = Number.isInteger(finding.endLineNumber) ? finding.endLineNumber : lineNumber;
96
+ const snippetHash = sha256(String(finding.snippetPreview || finding.snippet || ''));
97
+ const fingerprint = sha256(stableStringify({
98
+ ruleId: finding.ruleId || 'unknown_rule',
99
+ filePath: relativePath,
100
+ lineNumber,
101
+ snippetHash,
102
+ }));
103
+ const ruleId = safeText(finding.ruleId, 'unknown_rule', 80);
104
+ const ruleName = safeText(finding.ruleName, ruleId, 160);
105
+ const severity = safeText(finding.severity, 'medium', 20);
106
+ const category = safeToken(finding.category, 'general', 80);
107
+ const snippetPreview = safeSnippetPreview(
108
+ finding.snippetPreview || finding.snippet,
109
+ `Preview withheld for ${ruleId}; inspect local source at ${relativePath}:${lineNumber || 1}.`,
110
+ );
111
+
112
+ return {
113
+ id: `finding-${fingerprint.slice(0, 48)}`,
114
+ ruleId,
115
+ ruleName,
116
+ severity,
117
+ category,
118
+ title: `${ruleId}: ${ruleName}`.slice(0, 200),
119
+ status: 'open',
120
+ source: {
121
+ filePath: relativePath,
122
+ lineNumber,
123
+ endLineNumber,
124
+ language: finding.lang || undefined,
125
+ },
126
+ fingerprint,
127
+ snippetPreview,
128
+ fix: safeText(finding.fix, 'Review and fix this finding in local source.', 500),
129
+ evidence: [{
130
+ type: 'finding_location',
131
+ label: `${ruleId} location`,
132
+ preview: `${relativePath}:${lineNumber || 1}`,
133
+ summary: `${severity} ${category} finding from ${ruleName}.`,
134
+ metadata: {
135
+ ruleId,
136
+ filePath: relativePath,
137
+ lineNumber,
138
+ endLineNumber,
139
+ snippetHash,
140
+ },
141
+ }],
142
+ metadata: {
143
+ normalizedBy: DETERMINISTIC_SCAN_STEPPER_ID,
144
+ scannerSurface: 'mcp-bundled',
145
+ snippetHash,
146
+ },
147
+ };
148
+ }
149
+
150
+ function normalizeFindings(rootPath, fileResults) {
151
+ return fileResults.flatMap((fileResult) =>
152
+ (fileResult.result.findings || []).map((finding) =>
153
+ normalizeFinding({
154
+ rootPath,
155
+ filePath: fileResult.filePath,
156
+ finding: { ...finding, lang: fileResult.lang },
157
+ })));
158
+ }
159
+
160
+ function normalizeTopRiskFiles(rootPath, topRiskFiles) {
161
+ return topRiskFiles.map((item) => ({
162
+ filePath: safeRelativePath(rootPath, item.filePath),
163
+ score: item.score,
164
+ findings: item.findings,
165
+ critical: item.critical,
166
+ high: item.high,
167
+ }));
168
+ }
169
+
170
+ export function hashDeterministicScanArtifact(artifact) {
171
+ assertSourceSafePayload(artifact, 'deterministicScanArtifact');
172
+ return sha256(stableStringify(artifact));
173
+ }
174
+
175
+ export async function buildDeterministicScanArtifact({
176
+ rootPath,
177
+ includeGlobs = DEFAULT_INCLUDE,
178
+ excludeGlobs = DEFAULT_EXCLUDE,
179
+ maxFiles = DEFAULT_MAX_FILES,
180
+ generatedAt = nowIso(),
181
+ } = {}) {
182
+ if (!rootPath || typeof rootPath !== 'string') throw new Error('Deterministic Scan requires rootPath');
183
+ const resolvedRoot = path.resolve(rootPath);
184
+ const stat = await fs.stat(resolvedRoot);
185
+ if (!stat.isDirectory()) throw new Error(`Deterministic Scan rootPath is not a directory: ${resolvedRoot}`);
186
+
187
+ const effectiveExcludeGlobs = [...new Set([...excludeGlobs, ...LOCAL_ASSURANCE_EXCLUDE])];
188
+ const scan = await gatherRepoScan(resolvedRoot, includeGlobs, effectiveExcludeGlobs, maxFiles);
189
+ const findings = normalizeFindings(resolvedRoot, scan.fileResults);
190
+ const bySeverity = findingCounts(findings);
191
+ const engine = getRuleEngineMetadata('mcp-bundled');
192
+ const artifact = {
193
+ schemaVersion: DETERMINISTIC_SCAN_SCHEMA_VERSION,
194
+ generatedAt,
195
+ root: {
196
+ name: path.basename(resolvedRoot),
197
+ pathHash: sha256(resolvedRoot.toLowerCase()),
198
+ },
199
+ scanMode: 'local_mcp_rule_engine',
200
+ limits: {
201
+ maxFiles,
202
+ matchedFiles: scan.matchedFiles.length,
203
+ scannedFiles: scan.limitedFiles.length,
204
+ cappedByMaxFiles: scan.matchedFiles.length > maxFiles,
205
+ },
206
+ includeGlobs,
207
+ excludeGlobs: effectiveExcludeGlobs,
208
+ engine,
209
+ summary: {
210
+ ...scan.aggregate.summary,
211
+ bySeverity,
212
+ totalIssues: findings.length,
213
+ },
214
+ grade: getGrade(scan.aggregate.summary.score),
215
+ checklist: scan.aggregate.checklist,
216
+ findings,
217
+ topRiskFiles: normalizeTopRiskFiles(resolvedRoot, scan.topRiskFiles),
218
+ privacy: {
219
+ rawSourceStored: false,
220
+ snippetPreviewsCapped: true,
221
+ secretValuesRedacted: true,
222
+ artifactStorage: 'local_metadata_and_hash_refs',
223
+ },
224
+ };
225
+
226
+ assertSourceSafePayload(artifact, 'deterministicScanArtifact');
227
+ return cloneSourceSafe(artifact, 'deterministicScanArtifact');
228
+ }
229
+
230
+ export async function writeDeterministicScanArtifact({ rootPath, runId, artifact }) {
231
+ assertSourceSafePayload(artifact, 'deterministicScanArtifact');
232
+ const safeRunId = validateRunIdSegment(runId, 'runId');
233
+ const relativeUri = `.vibesecur/deep-scans/${safeRunId}/deterministic-scan.json`;
234
+ const target = path.join(path.resolve(rootPath), relativeUri);
235
+ await fs.mkdir(path.dirname(target), { recursive: true });
236
+ const serialized = `${JSON.stringify(stableSort(artifact), null, 2)}\n`;
237
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
238
+ await fs.writeFile(tmp, serialized, 'utf8');
239
+ await fs.rename(tmp, target);
240
+ return {
241
+ uri: relativeUri,
242
+ hash: sha256(serialized),
243
+ };
244
+ }
245
+
246
+ export function deterministicScanStepper() {
247
+ return {
248
+ id: DETERMINISTIC_SCAN_STEPPER_ID,
249
+ version: DETERMINISTIC_SCAN_STEPPER_VERSION,
250
+ title: 'Deterministic Rule Scan Stepper',
251
+ category: 'deterministic_rule_scan',
252
+ requiredInputs: ['bound_project_root'],
253
+ producedArtifacts: ['deterministic_scan'],
254
+ defaultTimeoutMs: 60000,
255
+ async run({ runId, config = {}, tools = {} }) {
256
+ const rootPath = tools.rootPath || config.rootPath || '.';
257
+ const startedAt = nowIso();
258
+ const artifact = await buildDeterministicScanArtifact({
259
+ rootPath,
260
+ generatedAt: config.generatedAt || startedAt,
261
+ includeGlobs: Array.isArray(config.includeGlobs) ? config.includeGlobs : DEFAULT_INCLUDE,
262
+ excludeGlobs: Array.isArray(config.excludeGlobs) ? config.excludeGlobs : DEFAULT_EXCLUDE,
263
+ maxFiles: Number.isFinite(Number(config.maxFiles)) ? Number(config.maxFiles) : DEFAULT_MAX_FILES,
264
+ });
265
+ const contentHash = hashDeterministicScanArtifact(artifact);
266
+ const written = await writeDeterministicScanArtifact({ rootPath, runId, artifact });
267
+ const safeRunId = validateRunIdSegment(runId, 'runId');
268
+ const ref = createArtifactRef({
269
+ id: `artifact-${safeRunId}-deterministic-scan`,
270
+ type: 'deterministic_scan',
271
+ storage: 'local',
272
+ uri: written.uri,
273
+ hash: written.hash,
274
+ preview: 'Deterministic scan metadata: normalized findings, counts, receipts, and engine metadata only.',
275
+ metadata: {
276
+ schemaVersion: DETERMINISTIC_SCAN_SCHEMA_VERSION,
277
+ contentHash,
278
+ generatedBy: DETERMINISTIC_SCAN_STEPPER_ID,
279
+ engineVersion: artifact.engine.version,
280
+ deterministicRuleCount: artifact.engine.counts.deterministic,
281
+ checklistCount: artifact.engine.counts.checklist,
282
+ totalIssues: artifact.summary.totalIssues,
283
+ },
284
+ });
285
+ const receipt = createReceiptRef({
286
+ id: `receipt-${safeRunId}-deterministic-scan`,
287
+ type: 'deterministic_scan',
288
+ source: 'mcp_deep_scan',
289
+ artifactId: ref.id,
290
+ artifactHash: ref.hash,
291
+ engine: artifact.engine,
292
+ summary: {
293
+ filesScanned: artifact.limits.scannedFiles,
294
+ totalIssues: artifact.summary.totalIssues,
295
+ bySeverity: artifact.summary.bySeverity,
296
+ score: artifact.summary.score,
297
+ grade: artifact.grade,
298
+ },
299
+ verification: {
300
+ benchmarkContinuity: 'covered_by_security_benchmark_gate',
301
+ sourceRetention: 'metadata_only',
302
+ },
303
+ metadata: {
304
+ schemaVersion: DETERMINISTIC_SCAN_SCHEMA_VERSION,
305
+ scanMode: artifact.scanMode,
306
+ contentHash,
307
+ },
308
+ createdAt: startedAt,
309
+ });
310
+
311
+ return {
312
+ stepperId: DETERMINISTIC_SCAN_STEPPER_ID,
313
+ version: DETERMINISTIC_SCAN_STEPPER_VERSION,
314
+ status: 'passed',
315
+ startedAt,
316
+ finishedAt: nowIso(),
317
+ evidence: [{
318
+ type: 'receipt',
319
+ label: 'Deterministic scan receipt',
320
+ hash: ref.hash,
321
+ preview: 'Receipt-backed deterministic scan artifact written locally.',
322
+ summary: `${artifact.summary.totalIssues} issue(s) across ${artifact.limits.scannedFiles} scanned file(s).`,
323
+ metadata: {
324
+ receiptId: receipt.id,
325
+ artifactId: ref.id,
326
+ contentHash,
327
+ },
328
+ }],
329
+ findings: artifact.findings,
330
+ artifacts: [ref],
331
+ receipts: [receipt],
332
+ summary: `Deterministic rule scan completed: ${artifact.summary.totalIssues} issue(s), score ${artifact.summary.score} (${artifact.grade}).`,
333
+ nextActions: ['Use normalized findings and receipt metadata for test planning, reports, passports, and reruns.'],
334
+ };
335
+ },
336
+ };
337
+ }
@@ -0,0 +1,109 @@
1
+ export {
2
+ APPROVAL_DECISIONS,
3
+ STEP_STATUSES,
4
+ createArtifactRef,
5
+ createReceiptRef,
6
+ validateApproval,
7
+ validateArtifactRef,
8
+ validateDeepScanState,
9
+ validateEvidence,
10
+ validateFinding,
11
+ validateReceiptRef,
12
+ validateStepResult,
13
+ } from './contracts.js';
14
+ export { StepperRegistry } from './registry.js';
15
+ export { LocalDeepScanStore } from './store.js';
16
+ export { DeepScanRuntime } from './runtime.js';
17
+ export { assertSourceSafePayload, cloneSourceSafe, findRawSourceField } from './sourceSafe.js';
18
+ export {
19
+ DETERMINISTIC_SCAN_SCHEMA_VERSION,
20
+ DETERMINISTIC_SCAN_STEPPER_ID,
21
+ DETERMINISTIC_SCAN_STEPPER_VERSION,
22
+ buildDeterministicScanArtifact,
23
+ deterministicScanStepper,
24
+ hashDeterministicScanArtifact,
25
+ writeDeterministicScanArtifact,
26
+ } from './deterministic-scan.js';
27
+ export {
28
+ PROJECT_MAP_SCHEMA_VERSION,
29
+ PROJECT_MAP_STEPPER_ID,
30
+ PROJECT_MAP_STEPPER_VERSION,
31
+ buildProjectMapArtifact,
32
+ hashProjectMapArtifact,
33
+ projectMapStepper,
34
+ writeProjectMapArtifact,
35
+ } from './project-map.js';
36
+ export {
37
+ RALPH_COMPARISON_SCHEMA_VERSION,
38
+ RALPH_COMPARISON_STATUSES,
39
+ RALPH_COMPARISON_STEPPER_ID,
40
+ RALPH_COMPARISON_STEPPER_VERSION,
41
+ buildRalphComparisonArtifact,
42
+ hashRalphComparisonArtifact,
43
+ ralphComparisonStepper,
44
+ validateRalphComparisonArtifact,
45
+ writeRalphComparisonArtifact,
46
+ } from './ralph-compare.js';
47
+ export {
48
+ DEFAULT_RALPH_LOOP_RETRY,
49
+ RALPH_ESCALATION_STATUSES,
50
+ RALPH_FAILURE_STATE_SCHEMA_VERSION,
51
+ RALPH_FAILURE_STATE_STEPPER_ID,
52
+ RALPH_FAILURE_STATE_STEPPER_VERSION,
53
+ buildRalphFailureStateArtifact,
54
+ hashRalphFailureStateArtifact,
55
+ ralphFailureTrackStepper,
56
+ validateRalphFailureStateArtifact,
57
+ writeRalphFailureStateArtifact,
58
+ } from './ralph-track.js';
59
+ export {
60
+ ACCEPTED_RISK_REGISTER_SCHEMA_VERSION,
61
+ ACCEPTED_RISK_STATE_SCHEMA_VERSION,
62
+ ACCEPTED_RISK_STATUSES,
63
+ ACCEPTED_RISK_STEPPER_ID,
64
+ ACCEPTED_RISK_STEPPER_VERSION,
65
+ ACCEPTED_RISK_REPORT_VISIBILITY,
66
+ ACCEPTED_RISK_HIGH_RISK_SEVERITIES,
67
+ appendAcceptedRiskRecord,
68
+ buildAcceptedRiskStateArtifact,
69
+ evaluateActiveAcceptedRisk,
70
+ hashAcceptedRiskStateArtifact,
71
+ indexAcceptedRiskForComparison,
72
+ ralphAcceptStepper,
73
+ readAcceptedRiskRegister,
74
+ validateAcceptedRiskRecord,
75
+ validateAcceptedRiskRegister,
76
+ validateAcceptedRiskStateArtifact,
77
+ writeAcceptedRiskStateArtifact,
78
+ } from './ralph-accept.js';
79
+ export {
80
+ AGENT_FIX_TASK_SCHEMA_VERSION,
81
+ AGENT_FIX_TASK_STATUSES,
82
+ AGENT_FIX_TASK_STEPPER_ID,
83
+ AGENT_FIX_TASK_STEPPER_VERSION,
84
+ buildAgentFixTaskArtifact,
85
+ hashAgentFixTaskArtifact,
86
+ ralphTaskStepper,
87
+ validateAgentFixTaskArtifact,
88
+ writeAgentFixTaskArtifact,
89
+ } from './ralph-tasks.js';
90
+ export {
91
+ SECURITY_TEST_PLAN_SCHEMA_VERSION,
92
+ SECURITY_TEST_PLAN_STATUSES,
93
+ SECURITY_TEST_PLAN_STATUS_SEMANTICS,
94
+ SECURITY_TEST_PLAN_STABLE_CANDIDATE_FIELDS,
95
+ SECURITY_TEST_PLAN_STEPPER_ID,
96
+ SECURITY_TEST_PLAN_STEPPER_VERSION,
97
+ buildSecurityTestPlanArtifact,
98
+ hashSecurityTestPlanArtifact,
99
+ securityTestPlanStepper,
100
+ validateSecurityTestPlanArtifact,
101
+ writeSecurityTestPlanArtifact,
102
+ } from './test-plan.js';
103
+ export { buildDeepScanStatus, nextActionForState } from './status.js';
104
+ export {
105
+ createSampleStepperRegistry,
106
+ sampleArtifactStepper,
107
+ sampleNeedsHumanStepper,
108
+ sampleNoopStepper,
109
+ } from './sample-steppers.js';