opstruth 0.1.3 → 0.2.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.
@@ -4,6 +4,16 @@ import { detectPackageManager, detectPackageScripts } from '../lib/detect.js';
4
4
  import { resolveProjectBoundary } from '../lib/boundary.js';
5
5
 
6
6
  const DEFAULT_SCRIPTS = ['typecheck', 'lint', 'test', 'build', 'ci'];
7
+ const INDIVIDUAL_SCRIPTS = ['lint', 'typecheck', 'test', 'build'];
8
+ const MUTATION_LIKE_SCRIPT = /\b(supabase\s+(?:secrets\s+set|db\s+push|functions\s+deploy)|wrangler\s+deploy|npm\s+publish|pnpm\s+publish|yarn\s+npm\s+publish|bun\s+publish|pg_cron|psql\s|curl\s+.*production)\b/i;
9
+
10
+ export const QUALITY_SIGNALS = [
11
+ { key: 'lint', script: 'lint', label: 'Lint' },
12
+ { key: 'typecheck', script: 'typecheck', label: 'Typecheck' },
13
+ { key: 'tests', script: 'test', label: 'Tests' },
14
+ { key: 'build', script: 'build', label: 'Build' },
15
+ { key: 'ci', script: 'ci', label: 'CI' }
16
+ ];
7
17
 
8
18
  export function isDefaultPlaceholderTestScript(script = '') {
9
19
  return /^echo\s+["']?Error:\s*no test specified["']?\s*(?:&&|;)\s*exit\s+1$/i.test(script.trim());
@@ -13,29 +23,140 @@ function isRunnableQualityScript(name, script) {
13
23
  return !(name === 'test' && isDefaultPlaceholderTestScript(script));
14
24
  }
15
25
 
16
- function runnerFor(manager) {
26
+ export function isMutationLikeQualityScript(script = '') {
27
+ return MUTATION_LIKE_SCRIPT.test(script);
28
+ }
29
+
30
+ export function qualityRunnerFor(manager) {
17
31
  if (manager === 'pnpm') return ['pnpm', ['run']];
18
32
  if (manager === 'yarn') return ['yarn', []];
19
33
  if (manager === 'bun') return ['bun', ['run']];
20
34
  return ['npm', ['run']];
21
35
  }
22
- export async function runQuality({ cwd = process.cwd(), continueOnFailure = false, strict = false, scripts: wantedScripts } = {}) {
36
+
37
+ function signalKeyForScript(script) {
38
+ return script === 'test' ? 'tests' : script;
39
+ }
40
+
41
+ function emptySignals(availableScripts) {
42
+ return Object.fromEntries(QUALITY_SIGNALS.map(({ key, script, label }) => {
43
+ const rawScript = availableScripts[script];
44
+ const configured = Object.prototype.hasOwnProperty.call(availableScripts, script);
45
+ const runnable = configured && isRunnableQualityScript(script, rawScript);
46
+ const requiresReview = runnable && isMutationLikeQualityScript(rawScript);
47
+ let status = 'not_configured';
48
+ let reason = 'package script not found';
49
+ if (configured && !runnable) {
50
+ status = 'skipped';
51
+ reason = 'default npm placeholder test script';
52
+ } else if (requiresReview) {
53
+ status = 'skipped';
54
+ reason = 'script contains mutation-like command and requires human review';
55
+ } else if (configured) {
56
+ status = 'not_verified';
57
+ reason = 'configured but not executed yet';
58
+ }
59
+ return [key, {
60
+ key,
61
+ label,
62
+ script,
63
+ configured,
64
+ status,
65
+ reason,
66
+ command: rawScript || null,
67
+ proofRoute: null,
68
+ exitCode: null,
69
+ durationMs: null,
70
+ logExcerpt: ''
71
+ }];
72
+ }));
73
+ }
74
+
75
+ function ciCoversScript(ciScript = '', script) {
76
+ const escaped = script.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
77
+ if (script === 'test' && /\bnpm\s+test\b|\bpnpm\s+test\b|\bbun\s+test\b|\byarn\s+test\b/.test(ciScript)) return true;
78
+ return new RegExp(`\\b(?:npm|pnpm|bun)\\s+run\\s+${escaped}\\b|\\byarn\\s+${escaped}\\b`).test(ciScript);
79
+ }
80
+
81
+ function checkStatusForSignal(status) {
82
+ if (status === 'passed') return 'pass';
83
+ if (status === 'failed' || status === 'timed_out') return 'fail';
84
+ if (status === 'skipped' || status === 'not_configured') return 'skipped';
85
+ return 'not_verified';
86
+ }
87
+
88
+ function signalMessage(signal) {
89
+ const bits = [signal.status];
90
+ if (signal.proofRoute) bits.push('via ' + signal.proofRoute);
91
+ if (signal.reason) bits.push(signal.reason);
92
+ return bits.join('; ');
93
+ }
94
+
95
+ function statusFromRun(run) {
96
+ if (run.exitCode === 0) return 'passed';
97
+ if (run.exitCode === 124 || run.signal) return 'timed_out';
98
+ return 'failed';
99
+ }
100
+
101
+ function failureText(script, signal) {
102
+ if (signal.status === 'timed_out') return script + ' timed out';
103
+ return script + ' failed';
104
+ }
105
+
106
+ async function executeScript({ script, signal, runner, baseArgs, cwd, timeoutMs, checks }) {
107
+ const run = await runCommand(runner, [...baseArgs, script], { cwd, timeoutMs });
108
+ const status = statusFromRun(run);
109
+ signal.status = status;
110
+ signal.reason = status === 'passed' ? 'configured script exited 0' : `configured script exited ${run.exitCode}`;
111
+ signal.proofRoute = 'direct';
112
+ signal.exitCode = run.exitCode;
113
+ signal.durationMs = run.durationMs;
114
+ signal.logExcerpt = excerpt((run.stdout || '') + '\n' + (run.stderr || ''));
115
+ checks.push({
116
+ name: 'package script ' + script,
117
+ command: run.command,
118
+ exitCode: run.exitCode,
119
+ durationMs: run.durationMs,
120
+ status: run.exitCode === 0 ? 'pass' : 'fail',
121
+ logExcerpt: signal.logExcerpt
122
+ });
123
+ return run;
124
+ }
125
+
126
+ function addSignalChecks(checks, signals) {
127
+ for (const signal of Object.values(signals)) {
128
+ checks.push({
129
+ name: 'quality signal ' + signal.key,
130
+ command: signal.proofRoute === 'direct' ? signal.command : null,
131
+ exitCode: signal.exitCode,
132
+ durationMs: signal.durationMs,
133
+ status: checkStatusForSignal(signal.status),
134
+ message: signalMessage(signal),
135
+ logExcerpt: signal.logExcerpt
136
+ });
137
+ }
138
+ }
139
+
140
+ function signalSummary(signals) {
141
+ return QUALITY_SIGNALS.map(({ key, label }) => `${label}: ${signals[key].status}${signals[key].proofRoute ? ' via ' + signals[key].proofRoute : ''}`).join('; ');
142
+ }
143
+
144
+ export async function runQuality({ cwd = process.cwd(), continueOnFailure = false, strict = false, scripts: wantedScripts, timeoutMs = 180000 } = {}) {
23
145
  const boundary = await resolveProjectBoundary(cwd);
24
146
  cwd = boundary.root;
25
147
  const packageManager = await detectPackageManager(cwd);
26
148
  const availableScripts = await detectPackageScripts(cwd);
27
149
  const requested = wantedScripts?.length ? wantedScripts : DEFAULT_SCRIPTS;
28
- const selected = requested.filter((name) => Object.prototype.hasOwnProperty.call(availableScripts, name) && isRunnableQualityScript(name, availableScripts[name]));
29
- const skippedScripts = requested.filter((name) => !Object.prototype.hasOwnProperty.call(availableScripts, name) || !isRunnableQualityScript(name, availableScripts[name]));
150
+ const explicitScripts = Boolean(wantedScripts?.length);
151
+ const signals = emptySignals(availableScripts);
30
152
  const checks = [];
31
153
  const warnings = [];
32
154
  const failures = [];
33
- const skipped = skippedScripts.map((name) => {
34
- if (Object.prototype.hasOwnProperty.call(availableScripts, name) && !isRunnableQualityScript(name, availableScripts[name])) {
35
- return 'default npm placeholder test script skipped';
36
- }
37
- return 'package script not found, skipped: ' + name;
38
- });
155
+ const skipped = [];
156
+ const notVerified = [];
157
+ const selected = [];
158
+ const [runner, baseArgs] = qualityRunnerFor(packageManager);
159
+
39
160
  if (boundary.message) skipped.push(boundary.message);
40
161
  if (boundary.isGitRepo) {
41
162
  const diff = await runCommand('git', ['diff', '--check'], { cwd, timeoutMs: 60000 });
@@ -44,23 +165,94 @@ export async function runQuality({ cwd = process.cwd(), continueOnFailure = fals
44
165
  } else {
45
166
  skipped.push('git diff --check skipped because no git repository was detected');
46
167
  }
47
- const [runner, baseArgs] = runnerFor(packageManager);
48
- for (const script of selected) {
49
- if (failures.length && !continueOnFailure) break;
50
- const run = await runCommand(runner, [...baseArgs, script], { cwd, timeoutMs: 180000 });
51
- checks.push({ name: 'package script ' + script, command: run.command, exitCode: run.exitCode, durationMs: run.durationMs, status: run.exitCode === 0 ? 'pass' : 'fail', logExcerpt: excerpt((run.stdout || '') + '\n' + (run.stderr || '')) });
52
- if (run.exitCode !== 0) failures.push(script + ' failed');
168
+
169
+ const requestedRunnable = requested.filter((name) => Object.prototype.hasOwnProperty.call(availableScripts, name) && isRunnableQualityScript(name, availableScripts[name]));
170
+ const requestedUnsafe = requestedRunnable.filter((name) => isMutationLikeQualityScript(availableScripts[name]));
171
+ for (const name of requestedUnsafe) warnings.push(`package script ${name} requires review before execution`);
172
+
173
+ let executionStrategy = 'individual';
174
+ const ciScript = availableScripts.ci || '';
175
+ const ciAvailable = requested.includes('ci') && Object.prototype.hasOwnProperty.call(availableScripts, 'ci') && isRunnableQualityScript('ci', ciScript);
176
+ const ciSafe = ciAvailable && !isMutationLikeQualityScript(ciScript);
177
+ if (!explicitScripts && ciSafe) executionStrategy = 'ci';
178
+ if (!explicitScripts && ciAvailable && !ciSafe) executionStrategy = 'individual';
179
+
180
+ if (executionStrategy === 'ci') {
181
+ selected.push('ci');
182
+ const run = await executeScript({ script: 'ci', signal: signals.ci, runner, baseArgs, cwd, timeoutMs, checks });
183
+ for (const script of INDIVIDUAL_SCRIPTS) {
184
+ const signal = signals[signalKeyForScript(script)];
185
+ if (!signal.configured || signal.status === 'skipped') continue;
186
+ if (ciCoversScript(ciScript, script)) {
187
+ signal.status = run.exitCode === 0 ? 'passed' : 'not_verified';
188
+ signal.reason = run.exitCode === 0 ? 'covered by configured ci script' : 'ci did not pass, so individual result is not verified';
189
+ signal.proofRoute = 'ci';
190
+ signal.exitCode = run.exitCode === 0 ? 0 : null;
191
+ signal.durationMs = run.exitCode === 0 ? run.durationMs : null;
192
+ } else {
193
+ signal.status = 'not_verified';
194
+ signal.reason = 'configured script was not run because ci strategy did not explicitly cover it';
195
+ }
196
+ }
197
+ if (run.exitCode !== 0) failures.push(failureText('ci', signals.ci));
198
+ } else {
199
+ const scriptsToRun = requested.filter((name) => name !== 'ci' || explicitScripts).filter((name) => {
200
+ const raw = availableScripts[name];
201
+ if (!Object.prototype.hasOwnProperty.call(availableScripts, name)) return false;
202
+ if (!isRunnableQualityScript(name, raw)) return false;
203
+ if (isMutationLikeQualityScript(raw)) return false;
204
+ return QUALITY_SIGNALS.some((signal) => signal.script === name);
205
+ });
206
+ for (const script of scriptsToRun) {
207
+ if (failures.length && !continueOnFailure) break;
208
+ selected.push(script);
209
+ const signal = signals[signalKeyForScript(script)];
210
+ await executeScript({ script, signal, runner, baseArgs, cwd, timeoutMs, checks });
211
+ if (signal.status === 'failed' || signal.status === 'timed_out') failures.push(failureText(script, signal));
212
+ }
213
+ }
214
+
215
+ for (const signal of Object.values(signals)) {
216
+ if (signal.status === 'not_configured') skipped.push(`package script not found, skipped: ${signal.script}`);
217
+ if (signal.status === 'skipped') skipped.push(`${signal.script} skipped: ${signal.reason}`);
218
+ if (signal.status === 'not_verified' && signal.configured) notVerified.push(`${signal.script} configured but not verified: ${signal.reason}`);
53
219
  }
54
220
  if (!Object.keys(availableScripts).length) skipped.push('No package.json scripts detected');
221
+
222
+ addSignalChecks(checks, signals);
223
+
224
+ const skippedScripts = Object.values(signals)
225
+ .filter((signal) => ['not_configured', 'skipped'].includes(signal.status))
226
+ .map((signal) => signal.script);
227
+
55
228
  const result = createResult('quality', failures.length ? 'fail' : warnings.length ? 'warn' : 'pass', {
56
- summary: 'Safe local quality gates ran only scripts that exist in package.json. Missing scripts were skipped, not failed.',
57
- verified: [boundary.isGitRepo ? 'git diff --check executed' : 'git diff --check skipped outside git repository', 'package.json scripts inspected', 'Existing quality scripts selected: ' + (selected.length ? selected.join(', ') : 'none')],
229
+ summary: `Safe local quality gates reported distinct proof signals. Execution strategy: ${executionStrategy}.`,
230
+ verified: [
231
+ boundary.isGitRepo ? 'git diff --check executed' : 'git diff --check skipped outside git repository',
232
+ 'package.json scripts inspected',
233
+ 'Quality proof signals: ' + signalSummary(signals),
234
+ 'Existing quality scripts selected: ' + (selected.length ? selected.join(', ') : 'none')
235
+ ],
58
236
  warnings,
59
237
  failures,
60
238
  skipped,
239
+ notVerified,
61
240
  checks,
62
- data: { boundary, packageManager, availableScripts, requestedScripts: requested, selectedScripts: selected, skippedScripts },
63
- nextSafeStep: failures.length ? 'Fix the failing quality command and rerun opstruth quality.' : 'Attach quality output to the evidence pack.'
241
+ data: {
242
+ boundary,
243
+ packageManager,
244
+ availableScripts,
245
+ requestedScripts: requested,
246
+ selectedScripts: selected,
247
+ skippedScripts,
248
+ quality: {
249
+ executionStrategy,
250
+ runner,
251
+ baseArgs,
252
+ signals
253
+ }
254
+ },
255
+ nextSafeStep: failures.length ? 'Fix the failing quality signal and rerun opstruth quality.' : 'Attach quality signal output to the evidence pack.'
64
256
  });
65
257
  return finalizeStatus(result, { strict });
66
258
  }
@@ -4,6 +4,38 @@ import { loadRoutesConfig, findDefaultRoutesConfig } from '../lib/config.js';
4
4
  import { resolveProjectBoundary } from '../lib/boundary.js';
5
5
 
6
6
  const DEFAULT_HEADERS = ['content-security-policy', 'strict-transport-security', 'x-frame-options', 'referrer-policy'];
7
+
8
+ export function isLocalRouteUrl(value) {
9
+ try {
10
+ const hostname = new URL(value).hostname.replace(/^\[|\]$/g, '').toLowerCase();
11
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ export function buildMissingHeaderFinding({ url, routePath, missingHeaders = [], evidence = [] } = {}) {
18
+ if (!missingHeaders.length) return null;
19
+ const localPreview = isLocalRouteUrl(url);
20
+ const finding = localPreview
21
+ ? `${routePath} local preview missing headers: ${missingHeaders.join(', ')}`
22
+ : `${routePath} missing headers: ${missingHeaders.join(', ')}`;
23
+
24
+ return createFinding({
25
+ status: 'warn',
26
+ area: 'routes',
27
+ title: localPreview ? 'Local preview security headers missing' : 'Route security headers missing',
28
+ finding,
29
+ evidence,
30
+ whyItMatters: localPreview
31
+ ? 'The local preview response does not include the expected security headers. Local development and preview servers may differ from the deployed hosting layer. Production headers remain Not Verified until a production URL is checked.'
32
+ : 'Missing browser security headers can weaken runtime protection even when the checked route is available.',
33
+ nextSafeStep: localPreview
34
+ ? 'Check an explicitly supplied production URL before drawing production conclusions, and configure local preview headers only when they are useful for parity.'
35
+ : 'Add the missing headers in the app or hosting layer and rerun route probes.'
36
+ });
37
+ }
38
+
7
39
  export async function runRoutes({ cwd = process.cwd(), baseUrl, routesFile, strict = false } = {}) {
8
40
  const boundary = await resolveProjectBoundary(cwd);
9
41
  cwd = boundary.root;
@@ -19,6 +51,7 @@ export async function runRoutes({ cwd = process.cwd(), baseUrl, routesFile, stri
19
51
  const warnings = [];
20
52
  const failures = [];
21
53
  const findings = [];
54
+ const localPreview = isLocalRouteUrl(finalBase);
22
55
  for (const route of routes) {
23
56
  const url = new URL(route.path, finalBase).toString();
24
57
  const probe = await probeUrl(url, { method: route.method || 'HEAD' });
@@ -38,12 +71,30 @@ export async function runRoutes({ cwd = process.cwd(), baseUrl, routesFile, stri
38
71
  failures.push(message);
39
72
  findings.push(createFinding({ status: 'fail', area: 'routes', title: 'Route status did not match expectation', finding: message, evidence, whyItMatters: 'A route that fails a read-only smoke check may block users or downstream health checks.', nextSafeStep: 'Inspect the route and rerun without deploying from opstruth.' }));
40
73
  }
41
- if (missingHeaders.length) {
42
- const message = `${route.path} missing headers: ${missingHeaders.join(', ')}`;
43
- warnings.push(message);
44
- findings.push(createFinding({ status: 'warn', area: 'routes', title: 'Route security headers missing', finding: message, evidence, whyItMatters: 'Missing browser security headers can weaken runtime protection even when the route is available.', nextSafeStep: 'Add the missing headers in the app or hosting layer and rerun route probes.' }));
74
+ const headerFinding = buildMissingHeaderFinding({ url, routePath: route.path, missingHeaders, evidence });
75
+ if (headerFinding) {
76
+ warnings.push(headerFinding.finding);
77
+ findings.push(headerFinding);
45
78
  }
46
79
  checks.push({ name: `${route.method || 'HEAD'} ${route.path}`, status: okStatus ? missingHeaders.length ? 'warn' : 'pass' : 'fail', message: `status=${probe.status || 'error'} latency=${probe.latencyMs}ms`, data: probe });
47
80
  }
48
- return finalizeStatus(createResult('routes', failures.length ? 'fail' : warnings.length ? 'warn' : 'pass', { summary: 'HEAD/GET public route smoke matrix completed.', verified: ['Project boundary: ' + boundary.root, 'Routes checked with read-only HEAD/GET requests', 'Response status, redirects, headers, and latency captured'], warnings, failures, findings, checks, data: { boundary, baseUrl: finalBase, routes, requiredHeaders }, nextSafeStep: failures.length ? 'Investigate failing routes without deploying from opstruth.' : 'Add missing required headers or attach route evidence.' }), { strict });
81
+ return finalizeStatus(createResult('routes', failures.length ? 'fail' : warnings.length ? 'warn' : 'pass', {
82
+ summary: localPreview ? 'HEAD/GET local preview route smoke matrix completed.' : 'HEAD/GET public route smoke matrix completed.',
83
+ verified: [
84
+ 'Project boundary: ' + boundary.root,
85
+ localPreview ? 'Local preview routes checked with read-only HEAD/GET requests' : 'Routes checked with read-only HEAD/GET requests',
86
+ 'Response status, redirects, headers, and latency captured'
87
+ ],
88
+ warnings,
89
+ failures,
90
+ findings,
91
+ checks,
92
+ notVerified: localPreview ? ['Production security headers were not checked because only a local preview URL was probed'] : [],
93
+ data: { boundary, baseUrl: finalBase, routes, requiredHeaders, scope: localPreview ? 'local_preview' : 'remote' },
94
+ nextSafeStep: failures.length
95
+ ? 'Investigate failing routes without deploying from opstruth.'
96
+ : localPreview
97
+ ? 'Check an explicit production URL to verify deployed security headers.'
98
+ : 'Add missing required headers or attach route evidence.'
99
+ }), { strict });
49
100
  }
@@ -1,41 +1,58 @@
1
1
  import { createFinding, createResult, finalizeStatus } from '../lib/result.js';
2
- import { scanRiskyReferences } from '../lib/scan.js';
2
+ import { formatSecretSummary, scanRiskyReferencesDetailed } from '../lib/scan.js';
3
3
  import { resolveProjectBoundary } from '../lib/boundary.js';
4
4
  import { loadOpstruthConfig } from '../lib/config.js';
5
5
 
6
+ function secretFindingTitle(item) {
7
+ if (item.category === 'actionable_source_finding') return 'Actionable secret finding';
8
+ if (item.category === 'unknown_requires_review') return 'Unknown token-like content';
9
+ return 'Secret/reference classification';
10
+ }
11
+
6
12
  export async function runSecrets({ cwd = process.cwd(), strict = false } = {}) {
7
13
  const boundary = await resolveProjectBoundary(cwd);
8
14
  const loaded = await loadOpstruthConfig(boundary.root);
9
15
  const secretConfig = loaded.config?.secrets || {};
10
- const findings = await scanRiskyReferences(boundary.root, {
16
+ const scan = await scanRiskyReferencesDetailed(boundary.root, {
11
17
  allowlistPaths: secretConfig.allowlistPaths || [],
12
18
  allowlistPatterns: secretConfig.allowlistPatterns || []
13
19
  });
14
- const findingObjects = findings.map((item) => createFinding({
20
+ const findingObjects = scan.findings.map((item) => createFinding({
15
21
  status: 'warn',
16
22
  area: 'secrets',
17
- title: 'Risky secret or auth reference',
23
+ title: secretFindingTitle(item),
18
24
  finding: `${item.file}:${item.line} matched ${item.pattern}`,
19
25
  evidence: [
20
26
  'file: ' + item.file,
21
27
  'line: ' + item.line,
22
28
  'pattern: ' + item.pattern,
29
+ 'category: ' + item.category,
30
+ 'severity: ' + item.severity,
23
31
  'kind: ' + item.kind,
24
32
  'context: ' + item.context,
25
33
  'redacted preview: ' + item.preview
26
34
  ],
27
35
  whyItMatters: 'Secret-like values and service-role references can create account, data, or infrastructure exposure if committed or exposed to browsers.',
28
- nextSafeStep: 'Confirm whether this is a harmless reference. Move real secrets to secret storage and keep only names/placeholders in source.'
36
+ nextSafeStep: 'Review actionable and unknown findings first. Move real secrets to secret storage and keep only names/placeholders in source.'
29
37
  }));
30
- const result = createResult('secrets', loaded.warning || findings.length ? 'warn' : 'pass', {
31
- summary: 'Redacted risky secret/reference scan completed. .env file contents are skipped.',
32
- verified: ['Project boundary scanned: ' + boundary.root, 'Source files scanned with redaction', '.env contents were not printed'],
38
+ const secretSummary = formatSecretSummary(scan.summary);
39
+ const result = createResult('secrets', loaded.warning || scan.findings.length ? 'warn' : 'pass', {
40
+ summary: 'Redacted secret/reference scan completed with grouped categories. .env file contents are skipped unless tracked.',
41
+ verified: ['Project boundary scanned: ' + boundary.root, 'Source files scanned with redaction', '.env contents were not printed', 'Secret references grouped: ' + secretSummary],
33
42
  warnings: [...(loaded.warning ? [loaded.warning] : []), ...findingObjects.map((finding) => finding.finding)],
34
43
  findings: findingObjects,
35
44
  skipped: boundary.message ? [boundary.message] : [],
36
- checks: [{ name: 'secret reference scan', status: findings.length ? 'warn' : 'pass', message: findings.length + ' finding(s)' }],
37
- data: { boundary, configFile: loaded.file, allowlistPaths: secretConfig.allowlistPaths || [], allowlistPatterns: secretConfig.allowlistPatterns || [], findings },
38
- nextSafeStep: findings.length ? 'Review whether each reference is expected and move real secrets to safe storage.' : 'Keep secrets out of source and rerun before publishing.'
45
+ checks: [{ name: 'secret reference scan', status: scan.findings.length ? 'warn' : 'pass', message: secretSummary }],
46
+ data: {
47
+ boundary,
48
+ configFile: loaded.file,
49
+ allowlistPaths: secretConfig.allowlistPaths || [],
50
+ allowlistPatterns: secretConfig.allowlistPatterns || [],
51
+ secretSummary: scan.summary,
52
+ classifiedFindings: scan.records,
53
+ findings: scan.findings
54
+ },
55
+ nextSafeStep: scan.findings.length ? 'Review actionable and unknown findings first; treat documentation references and placeholders as context unless they contain real values.' : 'Keep secrets out of source and rerun before publishing.'
39
56
  });
40
57
  return finalizeStatus(result, { strict });
41
58
  }