@veraxhq/verax 0.1.0 → 0.2.1
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/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +24 -36
- package/src/cli/commands/default.js +681 -0
- package/src/cli/commands/doctor.js +197 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +586 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +297 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +110 -0
- package/src/cli/util/expectation-extractor.js +388 -0
- package/src/cli/util/findings-writer.js +32 -0
- package/src/cli/util/idgen.js +87 -0
- package/src/cli/util/learn-writer.js +39 -0
- package/src/cli/util/observation-engine.js +412 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +30 -0
- package/src/cli/util/project-discovery.js +297 -0
- package/src/cli/util/project-writer.js +26 -0
- package/src/cli/util/redact.js +128 -0
- package/src/cli/util/run-id.js +30 -0
- package/src/cli/util/runtime-budget.js +147 -0
- package/src/cli/util/summary-writer.js +43 -0
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -0
- package/src/verax/cli/ci-summary.js +35 -0
- package/src/verax/cli/context-explanation.js +89 -0
- package/src/verax/cli/doctor.js +277 -0
- package/src/verax/cli/error-normalizer.js +154 -0
- package/src/verax/cli/explain-output.js +105 -0
- package/src/verax/cli/finding-explainer.js +130 -0
- package/src/verax/cli/init.js +237 -0
- package/src/verax/cli/run-overview.js +163 -0
- package/src/verax/cli/url-safety.js +111 -0
- package/src/verax/cli/wizard.js +109 -0
- package/src/verax/cli/zero-findings-explainer.js +57 -0
- package/src/verax/cli/zero-interaction-explainer.js +127 -0
- package/src/verax/core/action-classifier.js +86 -0
- package/src/verax/core/budget-engine.js +218 -0
- package/src/verax/core/canonical-outcomes.js +157 -0
- package/src/verax/core/decision-snapshot.js +335 -0
- package/src/verax/core/determinism-model.js +432 -0
- package/src/verax/core/incremental-store.js +245 -0
- package/src/verax/core/invariants.js +356 -0
- package/src/verax/core/promise-model.js +230 -0
- package/src/verax/core/replay-validator.js +350 -0
- package/src/verax/core/replay.js +222 -0
- package/src/verax/core/run-id.js +175 -0
- package/src/verax/core/run-manifest.js +99 -0
- package/src/verax/core/silence-impact.js +369 -0
- package/src/verax/core/silence-model.js +523 -0
- package/src/verax/detect/comparison.js +7 -34
- package/src/verax/detect/confidence-engine.js +764 -329
- package/src/verax/detect/detection-engine.js +293 -0
- package/src/verax/detect/evidence-index.js +127 -0
- package/src/verax/detect/expectation-model.js +241 -168
- package/src/verax/detect/explanation-helpers.js +187 -0
- package/src/verax/detect/finding-detector.js +450 -0
- package/src/verax/detect/findings-writer.js +41 -12
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +200 -288
- package/src/verax/detect/interactive-findings.js +612 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/verdict-engine.js +561 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +103 -15
- package/src/verax/intel/effect-detector.js +368 -0
- package/src/verax/intel/handler-mapper.js +249 -0
- package/src/verax/intel/index.js +281 -0
- package/src/verax/intel/route-extractor.js +280 -0
- package/src/verax/intel/ts-program.js +256 -0
- package/src/verax/intel/vue-navigation-extractor.js +642 -0
- package/src/verax/intel/vue-router-extractor.js +325 -0
- package/src/verax/learn/action-contract-extractor.js +338 -104
- package/src/verax/learn/ast-contract-extractor.js +148 -6
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +122 -58
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +28 -97
- package/src/verax/learn/route-validator.js +8 -7
- package/src/verax/learn/state-extractor.js +212 -0
- package/src/verax/learn/static-extractor-navigation.js +114 -0
- package/src/verax/learn/static-extractor-validation.js +88 -0
- package/src/verax/learn/static-extractor.js +119 -10
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +30 -6
- package/src/verax/observe/console-sensor.js +2 -18
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +513 -0
- package/src/verax/observe/flow-matcher.js +143 -0
- package/src/verax/observe/focus-sensor.js +196 -0
- package/src/verax/observe/human-driver.js +660 -273
- package/src/verax/observe/index.js +910 -26
- package/src/verax/observe/interaction-discovery.js +378 -15
- package/src/verax/observe/interaction-runner.js +562 -197
- package/src/verax/observe/loading-sensor.js +145 -0
- package/src/verax/observe/navigation-sensor.js +255 -0
- package/src/verax/observe/network-sensor.js +55 -7
- package/src/verax/observe/observed-expectation-deriver.js +186 -0
- package/src/verax/observe/observed-expectation.js +305 -0
- package/src/verax/observe/page-frontier.js +234 -0
- package/src/verax/observe/settle.js +38 -17
- package/src/verax/observe/state-sensor.js +393 -0
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +73 -21
- package/src/verax/observe/ui-signal-sensor.js +143 -17
- package/src/verax/scan-summary-writer.js +80 -15
- package/src/verax/shared/artifact-manager.js +111 -9
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +169 -0
- package/src/verax/shared/dynamic-route-utils.js +224 -0
- package/src/verax/shared/expectation-coverage.js +44 -0
- package/src/verax/shared/expectation-prover.js +81 -0
- package/src/verax/shared/expectation-tracker.js +201 -0
- package/src/verax/shared/expectations-writer.js +60 -0
- package/src/verax/shared/first-run.js +44 -0
- package/src/verax/shared/progress-reporter.js +171 -0
- package/src/verax/shared/retry-policy.js +9 -1
- package/src/verax/shared/root-artifacts.js +49 -0
- package/src/verax/shared/scan-budget.js +86 -0
- package/src/verax/shared/url-normalizer.js +162 -0
- package/src/verax/shared/zip-artifacts.js +66 -0
- package/src/verax/validate/context-validator.js +244 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 6 — Finding Explanation
|
|
3
|
+
*
|
|
4
|
+
* Formats findings in a clear, human-readable way showing the chain:
|
|
5
|
+
* Expectation → Observation → Mismatch → Why this is a silent failure
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Format a single finding for console output
|
|
10
|
+
* @param {Object} finding - Finding object
|
|
11
|
+
* @param {Object} expectation - Related expectation (if available)
|
|
12
|
+
* @returns {string} Formatted finding text
|
|
13
|
+
*/
|
|
14
|
+
export function formatFinding(finding, expectation = null) {
|
|
15
|
+
const lines = [];
|
|
16
|
+
|
|
17
|
+
// Finding type and confidence
|
|
18
|
+
const confidenceLevel = finding.confidence?.level || 'UNKNOWN';
|
|
19
|
+
const confidenceScore = finding.confidence?.score || 0;
|
|
20
|
+
lines.push(` [${confidenceLevel} (${confidenceScore}%)] ${finding.type || 'unknown'}`);
|
|
21
|
+
|
|
22
|
+
// Expectation (what was promised)
|
|
23
|
+
if (expectation) {
|
|
24
|
+
let expectationDesc = '';
|
|
25
|
+
if (expectation.type === 'navigation') {
|
|
26
|
+
const target = expectation.raw?.targetPath || expectation.targetPath || 'route';
|
|
27
|
+
expectationDesc = `Expected navigation to ${target}`;
|
|
28
|
+
} else if (expectation.type === 'network_action') {
|
|
29
|
+
const method = expectation.raw?.method || 'request';
|
|
30
|
+
const url = expectation.raw?.urlPath || expectation.urlPath || 'endpoint';
|
|
31
|
+
expectationDesc = `Expected ${method} request to ${url}`;
|
|
32
|
+
} else if (expectation.type === 'state_action') {
|
|
33
|
+
expectationDesc = `Expected state mutation`;
|
|
34
|
+
} else {
|
|
35
|
+
expectationDesc = `Expected ${expectation.type}`;
|
|
36
|
+
}
|
|
37
|
+
lines.push(` └─ Expectation: ${expectationDesc}`);
|
|
38
|
+
} else if (finding.expectationId) {
|
|
39
|
+
lines.push(` └─ Expectation: Referenced expectation ${finding.expectationId}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Observation (what actually happened)
|
|
43
|
+
const interaction = finding.interaction || {};
|
|
44
|
+
if (interaction.type) {
|
|
45
|
+
let interactionDesc = `${interaction.type}`;
|
|
46
|
+
if (interaction.selector) {
|
|
47
|
+
interactionDesc += ` on "${interaction.label || interaction.selector}"`;
|
|
48
|
+
}
|
|
49
|
+
lines.push(` └─ Observation: User interacted (${interactionDesc})`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Mismatch (what went wrong)
|
|
53
|
+
const evidence = finding.evidence || {};
|
|
54
|
+
if (finding.type === 'silent_failure' || finding.type === 'navigation_silent_failure') {
|
|
55
|
+
if (!evidence.hasUrlChange && !evidence.hasVisibleChange) {
|
|
56
|
+
lines.push(` └─ Mismatch: No navigation occurred, no visible change`);
|
|
57
|
+
} else if (evidence.hasUrlChange && !evidence.hasVisibleChange) {
|
|
58
|
+
lines.push(` └─ Mismatch: URL changed but no visible feedback`);
|
|
59
|
+
} else {
|
|
60
|
+
lines.push(` └─ Mismatch: Expected navigation did not occur`);
|
|
61
|
+
}
|
|
62
|
+
} else if (finding.type === 'network_silent_failure') {
|
|
63
|
+
if (evidence.slowRequests && evidence.slowRequests > 0) {
|
|
64
|
+
lines.push(` └─ Mismatch: Request was slow (${evidence.slowRequests} slow request(s))`);
|
|
65
|
+
} else {
|
|
66
|
+
lines.push(` └─ Mismatch: Request failed or returned error with no user feedback`);
|
|
67
|
+
}
|
|
68
|
+
} else if (finding.type === 'missing_network_action') {
|
|
69
|
+
lines.push(` └─ Mismatch: Expected network request never fired`);
|
|
70
|
+
} else if (finding.type === 'missing_state_action') {
|
|
71
|
+
lines.push(` └─ Mismatch: Expected state change did not occur`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Why this is a silent failure
|
|
75
|
+
if (finding.reason) {
|
|
76
|
+
lines.push(` └─ Silent Failure: ${finding.reason}`);
|
|
77
|
+
} else {
|
|
78
|
+
lines.push(` └─ Silent Failure: User receives no feedback when expected behavior fails`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Source location if available
|
|
82
|
+
if (expectation?.source?.file) {
|
|
83
|
+
const sourceLine = expectation.source.line ? `:${expectation.source.line}` : '';
|
|
84
|
+
lines.push(` └─ Source: ${expectation.source.file}${sourceLine}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return lines.join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Print top findings with explanations
|
|
92
|
+
* @param {Array} findings - Array of finding objects
|
|
93
|
+
* @param {Array} expectations - Array of expectation objects (for lookup)
|
|
94
|
+
* @param {number} limit - Maximum number of findings to print
|
|
95
|
+
*/
|
|
96
|
+
export function printFindings(findings, expectations = [], limit = 5) {
|
|
97
|
+
if (!findings || findings.length === 0) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.error('\n' + '─'.repeat(60));
|
|
102
|
+
console.error(`Top Findings (${Math.min(findings.length, limit)} of ${findings.length})`);
|
|
103
|
+
console.error('─'.repeat(60));
|
|
104
|
+
|
|
105
|
+
const topFindings = findings.slice(0, limit);
|
|
106
|
+
|
|
107
|
+
// Create expectation lookup map
|
|
108
|
+
const expectationMap = new Map();
|
|
109
|
+
for (const exp of expectations) {
|
|
110
|
+
if (exp.id) {
|
|
111
|
+
expectationMap.set(exp.id, exp);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
topFindings.forEach((finding, index) => {
|
|
116
|
+
const expectation = finding.expectationId
|
|
117
|
+
? expectationMap.get(finding.expectationId)
|
|
118
|
+
: null;
|
|
119
|
+
|
|
120
|
+
console.error(`\n${index + 1}.`);
|
|
121
|
+
console.error(formatFinding(finding, expectation));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (findings.length > limit) {
|
|
125
|
+
console.error(`\n... and ${findings.length - limit} more (see findings.json)`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.error('─'.repeat(60) + '\n');
|
|
129
|
+
}
|
|
130
|
+
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 7 — VERAX Init
|
|
3
|
+
*
|
|
4
|
+
* Initializes VERAX configuration and templates.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { resolve } from 'path';
|
|
9
|
+
import { getDefaultConfig } from '../shared/config-loader.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Initialize VERAX configuration
|
|
13
|
+
* @param {Object} options - { projectRoot, yes, ciTemplate, flowTemplate }
|
|
14
|
+
* @returns {Promise<Object>} { created: string[], skipped: string[] }
|
|
15
|
+
*/
|
|
16
|
+
export async function runInit(options = {}) {
|
|
17
|
+
const {
|
|
18
|
+
projectRoot = process.cwd(),
|
|
19
|
+
yes = false,
|
|
20
|
+
ciTemplate = null,
|
|
21
|
+
flowTemplate = null
|
|
22
|
+
} = options;
|
|
23
|
+
|
|
24
|
+
const created = [];
|
|
25
|
+
const skipped = [];
|
|
26
|
+
|
|
27
|
+
// Create .verax directory if needed
|
|
28
|
+
const veraxDir = resolve(projectRoot, '.verax');
|
|
29
|
+
mkdirSync(veraxDir, { recursive: true });
|
|
30
|
+
|
|
31
|
+
// Create config.json
|
|
32
|
+
const configPath = resolve(veraxDir, 'config.json');
|
|
33
|
+
if (existsSync(configPath) && !yes) {
|
|
34
|
+
skipped.push('config.json');
|
|
35
|
+
} else {
|
|
36
|
+
const defaultConfig = getDefaultConfig();
|
|
37
|
+
writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + '\n');
|
|
38
|
+
created.push('config.json');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Create CI template if requested
|
|
42
|
+
if (ciTemplate === 'github') {
|
|
43
|
+
const workflowsDir = resolve(projectRoot, '.github', 'workflows');
|
|
44
|
+
mkdirSync(workflowsDir, { recursive: true });
|
|
45
|
+
|
|
46
|
+
const workflowFile = resolve(workflowsDir, 'verax-ci.yml');
|
|
47
|
+
if (existsSync(workflowFile) && !yes) {
|
|
48
|
+
skipped.push('.github/workflows/verax-ci.yml');
|
|
49
|
+
} else {
|
|
50
|
+
const workflowContent = `name: VERAX CI
|
|
51
|
+
|
|
52
|
+
on:
|
|
53
|
+
workflow_dispatch:
|
|
54
|
+
pull_request:
|
|
55
|
+
|
|
56
|
+
jobs:
|
|
57
|
+
verax-scan:
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
steps:
|
|
60
|
+
- name: Checkout
|
|
61
|
+
uses: actions/checkout@v4
|
|
62
|
+
|
|
63
|
+
- name: Setup Node.js
|
|
64
|
+
uses: actions/setup-node@v4
|
|
65
|
+
with:
|
|
66
|
+
node-version: '20'
|
|
67
|
+
cache: 'npm'
|
|
68
|
+
|
|
69
|
+
- name: Install dependencies
|
|
70
|
+
run: npm ci
|
|
71
|
+
|
|
72
|
+
- name: Install Playwright browsers
|
|
73
|
+
run: npx playwright install --with-deps chromium
|
|
74
|
+
|
|
75
|
+
- name: Start fixture server
|
|
76
|
+
id: fixture-server
|
|
77
|
+
run: |
|
|
78
|
+
node test/helpers/fixture-server.js &
|
|
79
|
+
SERVER_PID=$!
|
|
80
|
+
echo "SERVER_PID=$SERVER_PID" >> $GITHUB_ENV
|
|
81
|
+
sleep 3
|
|
82
|
+
echo "url=http://127.0.0.1:8888" >> $GITHUB_OUTPUT
|
|
83
|
+
working-directory: \${{ github.workspace }}
|
|
84
|
+
|
|
85
|
+
- name: Run VERAX CI scan
|
|
86
|
+
id: verax
|
|
87
|
+
run: |
|
|
88
|
+
npx @veraxhq/verax ci --url \${{ steps.fixture-server.outputs.url }} --projectRoot .
|
|
89
|
+
continue-on-error: true
|
|
90
|
+
|
|
91
|
+
- name: Stop fixture server
|
|
92
|
+
if: always()
|
|
93
|
+
run: |
|
|
94
|
+
if [ ! -z "$SERVER_PID" ]; then
|
|
95
|
+
kill $SERVER_PID 2>/dev/null || true
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
- name: Upload VERAX artifacts
|
|
99
|
+
if: always()
|
|
100
|
+
uses: actions/upload-artifact@v4
|
|
101
|
+
with:
|
|
102
|
+
name: verax-artifacts
|
|
103
|
+
path: |
|
|
104
|
+
.verax/runs/**/*
|
|
105
|
+
.verax/verax-run-*.zip
|
|
106
|
+
retention-days: 7
|
|
107
|
+
if-no-files-found: ignore
|
|
108
|
+
|
|
109
|
+
- name: Check VERAX exit code and fail gate
|
|
110
|
+
if: always()
|
|
111
|
+
run: |
|
|
112
|
+
EXIT_CODE=\${{ steps.verax.exitcode }}
|
|
113
|
+
if [ "$EXIT_CODE" == "" ]; then
|
|
114
|
+
EXIT_CODE=\${{ steps.verax.outcome == 'success' && 0 || 1 }}
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
if [ "$EXIT_CODE" == "0" ]; then
|
|
118
|
+
echo "✅ VERAX: VERIFIED - Scan passed"
|
|
119
|
+
elif [ "$EXIT_CODE" == "1" ]; then
|
|
120
|
+
echo "⚠️ VERAX: NO_EXPECTATIONS_FOUND or MEDIUM/LOW findings"
|
|
121
|
+
echo "Gate passes (non-blocking)"
|
|
122
|
+
elif [ "$EXIT_CODE" == "2" ]; then
|
|
123
|
+
echo "❌ VERAX: HIGH severity findings detected"
|
|
124
|
+
echo "Gate fails (blocking)"
|
|
125
|
+
exit 2
|
|
126
|
+
elif [ "$EXIT_CODE" == "4" ]; then
|
|
127
|
+
echo "❌ VERAX: INVALID_CONTEXT - URL does not match project"
|
|
128
|
+
echo "Gate fails (blocking)"
|
|
129
|
+
exit 4
|
|
130
|
+
elif [ "$EXIT_CODE" == "3" ]; then
|
|
131
|
+
echo "❌ VERAX: FATAL error"
|
|
132
|
+
echo "Gate fails (blocking)"
|
|
133
|
+
exit 3
|
|
134
|
+
else
|
|
135
|
+
echo "❌ VERAX: Unexpected exit code $EXIT_CODE"
|
|
136
|
+
exit $EXIT_CODE
|
|
137
|
+
fi
|
|
138
|
+
`;
|
|
139
|
+
writeFileSync(workflowFile, workflowContent);
|
|
140
|
+
created.push('.github/workflows/verax-ci.yml');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Create flow template if requested
|
|
145
|
+
if (flowTemplate === 'login') {
|
|
146
|
+
const flowsDir = resolve(projectRoot, 'flows');
|
|
147
|
+
mkdirSync(flowsDir, { recursive: true });
|
|
148
|
+
|
|
149
|
+
const flowFile = resolve(flowsDir, 'login.json');
|
|
150
|
+
if (existsSync(flowFile) && !yes) {
|
|
151
|
+
skipped.push('flows/login.json');
|
|
152
|
+
} else {
|
|
153
|
+
const flowContent = {
|
|
154
|
+
name: 'login',
|
|
155
|
+
description: 'User login flow',
|
|
156
|
+
steps: [
|
|
157
|
+
{
|
|
158
|
+
type: 'goto',
|
|
159
|
+
url: '${VERAX_BASE_URL}/login'
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
type: 'fill',
|
|
163
|
+
selector: 'input[name="email"]',
|
|
164
|
+
value: '${VERAX_TEST_EMAIL}'
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
type: 'fill',
|
|
168
|
+
selector: 'input[name="password"]',
|
|
169
|
+
value: '${VERAX_TEST_PASSWORD}',
|
|
170
|
+
isSecret: true
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
type: 'click',
|
|
174
|
+
selector: 'button[type="submit"]'
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
type: 'wait',
|
|
178
|
+
selector: '[data-testid="dashboard"]',
|
|
179
|
+
timeout: 5000
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
};
|
|
183
|
+
writeFileSync(flowFile, JSON.stringify(flowContent, null, 2) + '\n');
|
|
184
|
+
created.push('flows/login.json');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { created, skipped };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Print init results
|
|
193
|
+
* @param {Object} results - Init results
|
|
194
|
+
*/
|
|
195
|
+
export function printInitResults(results) {
|
|
196
|
+
console.error('\n' + '═'.repeat(60));
|
|
197
|
+
console.error('VERAX Init');
|
|
198
|
+
console.error('═'.repeat(60));
|
|
199
|
+
|
|
200
|
+
if (results.created.length > 0) {
|
|
201
|
+
console.error('\n✅ Created:');
|
|
202
|
+
results.created.forEach(file => {
|
|
203
|
+
console.error(` • ${file}`);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (results.skipped.length > 0) {
|
|
208
|
+
console.error('\n⏭️ Skipped (already exist):');
|
|
209
|
+
results.skipped.forEach(file => {
|
|
210
|
+
console.error(` • ${file}`);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (results.created.includes('config.json')) {
|
|
215
|
+
console.error('\n📝 Next Steps:');
|
|
216
|
+
console.error(' 1. Review .verax/config.json and adjust settings');
|
|
217
|
+
console.error(' 2. Run: verax doctor (to verify setup)');
|
|
218
|
+
console.error(' 3. Run: verax run --url <your-url>');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (results.created.includes('.github/workflows/verax-ci.yml')) {
|
|
222
|
+
console.error('\n🔧 CI Setup:');
|
|
223
|
+
console.error(' • Review .github/workflows/verax-ci.yml');
|
|
224
|
+
console.error(' • Update URL placeholder with your deployment URL');
|
|
225
|
+
console.error(' • Commit and push to enable VERAX in CI');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (results.created.includes('flows/login.json')) {
|
|
229
|
+
console.error('\n🔐 Flow Template:');
|
|
230
|
+
console.error(' • Review flows/login.json');
|
|
231
|
+
console.error(' • Set VERAX_TEST_EMAIL and VERAX_TEST_PASSWORD environment variables');
|
|
232
|
+
console.error(' • Note: Flow commands are not available in this version');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.error('═'.repeat(60) + '\n');
|
|
236
|
+
}
|
|
237
|
+
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 6 — Run Overview
|
|
3
|
+
*
|
|
4
|
+
* Generates a clear, human-readable overview of the scan run.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate run overview data structure
|
|
9
|
+
* @param {Object} context - Scan context
|
|
10
|
+
* @returns {Object} Overview data
|
|
11
|
+
*/
|
|
12
|
+
export function generateRunOverview(context = {}) {
|
|
13
|
+
const {
|
|
14
|
+
manifest = null,
|
|
15
|
+
expectationsSummary = { total: 0, navigation: 0, networkActions: 0, stateActions: 0 },
|
|
16
|
+
interactionsObserved = 0,
|
|
17
|
+
contextCheck = null,
|
|
18
|
+
verdict = 'UNKNOWN',
|
|
19
|
+
findingsCount = 0
|
|
20
|
+
} = context;
|
|
21
|
+
|
|
22
|
+
const projectType = manifest?.projectType || 'unknown';
|
|
23
|
+
const expectationsTotal = expectationsSummary.total || 0;
|
|
24
|
+
|
|
25
|
+
// Determine validation status
|
|
26
|
+
let validationStatus = 'skipped';
|
|
27
|
+
let validationReason = 'No routes to validate';
|
|
28
|
+
|
|
29
|
+
if (contextCheck && contextCheck.ran) {
|
|
30
|
+
if (contextCheck.verdict === 'VALID_CONTEXT') {
|
|
31
|
+
validationStatus = 'validated';
|
|
32
|
+
validationReason = `${contextCheck.matchedRoutesCount} routes matched`;
|
|
33
|
+
} else if (contextCheck.verdict === 'INVALID_CONTEXT') {
|
|
34
|
+
validationStatus = 'mismatch';
|
|
35
|
+
validationReason = 'URL does not match project';
|
|
36
|
+
} else if (contextCheck.verdict === 'INVALID_CONTEXT_FORCED') {
|
|
37
|
+
validationStatus = 'forced';
|
|
38
|
+
validationReason = 'Scan continued with --force despite mismatch';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Generate trust statement
|
|
43
|
+
let trustLevel = 'low';
|
|
44
|
+
const trustReasons = [];
|
|
45
|
+
|
|
46
|
+
if (verdict === 'VERIFIED') {
|
|
47
|
+
trustLevel = 'high';
|
|
48
|
+
trustReasons.push(`All ${expectationsTotal} expectations validated`);
|
|
49
|
+
trustReasons.push(`${interactionsObserved} interactions tested`);
|
|
50
|
+
if (validationStatus === 'validated') {
|
|
51
|
+
trustReasons.push('Context validated');
|
|
52
|
+
}
|
|
53
|
+
} else if (verdict === 'ISSUES_FOUND') {
|
|
54
|
+
trustLevel = 'high';
|
|
55
|
+
trustReasons.push(`${expectationsTotal} expectations analyzed`);
|
|
56
|
+
trustReasons.push(`${interactionsObserved} interactions tested`);
|
|
57
|
+
trustReasons.push(`${findingsCount} issue(s) detected`);
|
|
58
|
+
} else if (verdict === 'NO_EXPECTATIONS_FOUND') {
|
|
59
|
+
trustLevel = 'low';
|
|
60
|
+
trustReasons.push('No code-derived expectations found');
|
|
61
|
+
trustReasons.push('Cannot validate without expectations');
|
|
62
|
+
} else if (verdict === 'INVALID_CONTEXT' || verdict === 'INVALID_CONTEXT_FORCED') {
|
|
63
|
+
trustLevel = 'partial';
|
|
64
|
+
trustReasons.push(`${expectationsTotal} expectations found`);
|
|
65
|
+
if (validationStatus === 'forced') {
|
|
66
|
+
trustReasons.push('Context mismatch (forced scan)');
|
|
67
|
+
} else {
|
|
68
|
+
trustReasons.push('Context mismatch - scan stopped early');
|
|
69
|
+
}
|
|
70
|
+
if (interactionsObserved === 0) {
|
|
71
|
+
trustReasons.push('No interactions executed');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Adjust trust level based on interactions
|
|
76
|
+
if (expectationsTotal > 0 && interactionsObserved === 0 && verdict !== 'INVALID_CONTEXT') {
|
|
77
|
+
trustLevel = 'partial';
|
|
78
|
+
if (!trustReasons.some(r => r.includes('No interactions'))) {
|
|
79
|
+
trustReasons.push('No interactions executed - validation incomplete');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Explicitly mention skipped expectations
|
|
84
|
+
if (context?.expectationsUnused !== undefined && context.expectationsUnused > 0) {
|
|
85
|
+
if (trustLevel === 'high') {
|
|
86
|
+
trustLevel = 'partial';
|
|
87
|
+
}
|
|
88
|
+
trustReasons.push(`${context.expectationsUnused} expectation(s) unused - partial validation`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
projectType: projectType,
|
|
93
|
+
expectationsFound: expectationsTotal,
|
|
94
|
+
expectationsByType: {
|
|
95
|
+
navigation: expectationsSummary.navigation || 0,
|
|
96
|
+
networkActions: expectationsSummary.networkActions || 0,
|
|
97
|
+
stateActions: expectationsSummary.stateActions || 0
|
|
98
|
+
},
|
|
99
|
+
interactionsExecuted: interactionsObserved,
|
|
100
|
+
validationStatus: validationStatus,
|
|
101
|
+
validationReason: validationReason,
|
|
102
|
+
trustLevel: trustLevel,
|
|
103
|
+
trustReasons: trustReasons
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Print run overview console block
|
|
109
|
+
* @param {Object} overview - Overview data from generateRunOverview
|
|
110
|
+
* @param {boolean} isCI - Whether in CI mode
|
|
111
|
+
*/
|
|
112
|
+
export function printRunOverview(overview, isCI = false) {
|
|
113
|
+
if (isCI) {
|
|
114
|
+
// Compact CI format
|
|
115
|
+
console.error(`VERAX Run: ${overview.projectType} | ${overview.expectationsFound} expectations | ${overview.interactionsExecuted} interactions | ${overview.validationStatus}`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Full format
|
|
120
|
+
console.error('\n' + '═'.repeat(60));
|
|
121
|
+
console.error('Run Overview');
|
|
122
|
+
console.error('═'.repeat(60));
|
|
123
|
+
|
|
124
|
+
console.error(`Project Type: ${overview.projectType}`);
|
|
125
|
+
|
|
126
|
+
console.error(`\nExpectations Found: ${overview.expectationsFound}`);
|
|
127
|
+
if (overview.expectationsFound > 0) {
|
|
128
|
+
const types = [];
|
|
129
|
+
if (overview.expectationsByType.navigation > 0) {
|
|
130
|
+
types.push(`navigation (${overview.expectationsByType.navigation})`);
|
|
131
|
+
}
|
|
132
|
+
if (overview.expectationsByType.networkActions > 0) {
|
|
133
|
+
types.push(`network actions (${overview.expectationsByType.networkActions})`);
|
|
134
|
+
}
|
|
135
|
+
if (overview.expectationsByType.stateActions > 0) {
|
|
136
|
+
types.push(`state actions (${overview.expectationsByType.stateActions})`);
|
|
137
|
+
}
|
|
138
|
+
if (types.length > 0) {
|
|
139
|
+
console.error(` Types: ${types.join(', ')}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.error(`\nInteractions Executed: ${overview.interactionsExecuted}`);
|
|
144
|
+
|
|
145
|
+
console.error(`\nValidation Status: ${overview.validationStatus}`);
|
|
146
|
+
console.error(` ${overview.validationReason}`);
|
|
147
|
+
|
|
148
|
+
// Trust statement
|
|
149
|
+
console.error(`\nTrust Assessment: ${overview.trustLevel.toUpperCase()}`);
|
|
150
|
+
const trustPrefix = overview.trustLevel === 'high'
|
|
151
|
+
? 'This result is trustworthy because'
|
|
152
|
+
: overview.trustLevel === 'partial'
|
|
153
|
+
? 'This result is limited because'
|
|
154
|
+
: 'This result cannot be trusted because';
|
|
155
|
+
|
|
156
|
+
console.error(` ${trustPrefix}:`);
|
|
157
|
+
overview.trustReasons.forEach(reason => {
|
|
158
|
+
console.error(` • ${reason}`);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
console.error('═'.repeat(60) + '\n');
|
|
162
|
+
}
|
|
163
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 3 — URL Safety Checks
|
|
3
|
+
*
|
|
4
|
+
* Detects external/public URLs and requires confirmation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check if an IP address is private/local
|
|
9
|
+
* @param {string} ip - IP address
|
|
10
|
+
* @returns {boolean} True if private/local
|
|
11
|
+
*/
|
|
12
|
+
function isPrivateIP(ip) {
|
|
13
|
+
// localhost
|
|
14
|
+
if (ip === '127.0.0.1' || ip === '::1' || ip === 'localhost') {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Private IP ranges
|
|
19
|
+
const privateRanges = [
|
|
20
|
+
/^10\./, // 10.0.0.0/8
|
|
21
|
+
/^172\.(1[6-9]|2[0-9]|3[01])\./, // 172.16.0.0/12
|
|
22
|
+
/^192\.168\./, // 192.168.0.0/16
|
|
23
|
+
/^169\.254\./, // Link-local
|
|
24
|
+
/^fc00:/, // IPv6 private
|
|
25
|
+
/^fe80:/ // IPv6 link-local
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
return privateRanges.some(range => range.test(ip));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if URL is external/public (requires confirmation)
|
|
33
|
+
* @param {string} url - URL to check
|
|
34
|
+
* @returns {Object} { isExternal: boolean, hostname: string }
|
|
35
|
+
*/
|
|
36
|
+
export function checkUrlSafety(url) {
|
|
37
|
+
try {
|
|
38
|
+
const urlObj = new URL(url);
|
|
39
|
+
const hostname = urlObj.hostname;
|
|
40
|
+
|
|
41
|
+
// Check if localhost
|
|
42
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
|
43
|
+
return {
|
|
44
|
+
isExternal: false,
|
|
45
|
+
hostname: hostname
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check if private IP
|
|
50
|
+
if (isPrivateIP(hostname)) {
|
|
51
|
+
return {
|
|
52
|
+
isExternal: false,
|
|
53
|
+
hostname: hostname
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// All other hosts are considered external
|
|
58
|
+
return {
|
|
59
|
+
isExternal: true,
|
|
60
|
+
hostname: hostname
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// Invalid URL - let other validation catch it
|
|
64
|
+
return {
|
|
65
|
+
isExternal: false,
|
|
66
|
+
hostname: null
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @typedef {Object} ReadlineInterface
|
|
73
|
+
* @property {function(string): Promise<string>} question - Prompt user with question
|
|
74
|
+
* @property {function(): void} close - Close the readline interface
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @typedef {Object} ConfirmExternalUrlOptions
|
|
79
|
+
* @property {ReadlineInterface} [readlineInterface] - Readline interface (injectable)
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Prompt for external URL confirmation
|
|
84
|
+
* @param {string} hostname - Hostname to confirm
|
|
85
|
+
* @param {ConfirmExternalUrlOptions} [options={}] - Options
|
|
86
|
+
* @returns {Promise<boolean>} True if confirmed
|
|
87
|
+
*/
|
|
88
|
+
export async function confirmExternalUrl(hostname, options = {}) {
|
|
89
|
+
const rl = options.readlineInterface || null;
|
|
90
|
+
|
|
91
|
+
if (!rl) {
|
|
92
|
+
// Use readline if not injected
|
|
93
|
+
const readline = await import('readline/promises');
|
|
94
|
+
const rlInterface = readline.default.createInterface({
|
|
95
|
+
input: process.stdin,
|
|
96
|
+
output: process.stdout,
|
|
97
|
+
terminal: true
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const answer = await rlInterface.question(`This will scan a public site (${hostname}). Continue? (y/N): `);
|
|
102
|
+
return (answer.trim().toLowerCase() || 'n').startsWith('y');
|
|
103
|
+
} finally {
|
|
104
|
+
rlInterface.close();
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
const answer = await rl.question(`This will scan a public site (${hostname}). Continue? (y/N): `);
|
|
108
|
+
return (answer.trim().toLowerCase() || 'n').startsWith('y');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|