@veraxhq/verax 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 +21 -0
- package/README.md +237 -0
- package/bin/verax.js +452 -0
- package/package.json +57 -0
- package/src/verax/detect/comparison.js +69 -0
- package/src/verax/detect/confidence-engine.js +498 -0
- package/src/verax/detect/evidence-validator.js +33 -0
- package/src/verax/detect/expectation-model.js +204 -0
- package/src/verax/detect/findings-writer.js +31 -0
- package/src/verax/detect/index.js +397 -0
- package/src/verax/detect/skip-classifier.js +202 -0
- package/src/verax/flow/flow-engine.js +265 -0
- package/src/verax/flow/flow-spec.js +145 -0
- package/src/verax/flow/redaction.js +74 -0
- package/src/verax/index.js +97 -0
- package/src/verax/learn/action-contract-extractor.js +281 -0
- package/src/verax/learn/ast-contract-extractor.js +255 -0
- package/src/verax/learn/index.js +18 -0
- package/src/verax/learn/manifest-writer.js +97 -0
- package/src/verax/learn/project-detector.js +87 -0
- package/src/verax/learn/react-router-extractor.js +73 -0
- package/src/verax/learn/route-extractor.js +122 -0
- package/src/verax/learn/route-validator.js +215 -0
- package/src/verax/learn/source-instrumenter.js +214 -0
- package/src/verax/learn/static-extractor.js +222 -0
- package/src/verax/learn/truth-assessor.js +96 -0
- package/src/verax/learn/ts-contract-resolver.js +395 -0
- package/src/verax/observe/browser.js +22 -0
- package/src/verax/observe/console-sensor.js +166 -0
- package/src/verax/observe/dom-signature.js +23 -0
- package/src/verax/observe/domain-boundary.js +38 -0
- package/src/verax/observe/evidence-capture.js +5 -0
- package/src/verax/observe/human-driver.js +376 -0
- package/src/verax/observe/index.js +67 -0
- package/src/verax/observe/interaction-discovery.js +269 -0
- package/src/verax/observe/interaction-runner.js +410 -0
- package/src/verax/observe/network-sensor.js +173 -0
- package/src/verax/observe/selector-generator.js +74 -0
- package/src/verax/observe/settle.js +155 -0
- package/src/verax/observe/state-ui-sensor.js +200 -0
- package/src/verax/observe/traces-writer.js +82 -0
- package/src/verax/observe/ui-signal-sensor.js +197 -0
- package/src/verax/resolve-workspace-root.js +173 -0
- package/src/verax/scan-summary-writer.js +41 -0
- package/src/verax/shared/artifact-manager.js +139 -0
- package/src/verax/shared/caching.js +104 -0
- package/src/verax/shared/expectation-proof.js +4 -0
- package/src/verax/shared/redaction.js +227 -0
- package/src/verax/shared/retry-policy.js +89 -0
- package/src/verax/shared/timing-metrics.js +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 VERAX
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
🛡️ VERAX
|
|
2
|
+
|
|
3
|
+
VERAX detects silent user failures by comparing what your code promises with what users actually experience.
|
|
4
|
+
|
|
5
|
+
Silent failures don’t crash your site.
|
|
6
|
+
They don’t show errors.
|
|
7
|
+
They simply lose users quietly.
|
|
8
|
+
|
|
9
|
+
VERAX exists to surface those failures — with evidence, not guesses.
|
|
10
|
+
|
|
11
|
+
🤔 What is VERAX?
|
|
12
|
+
|
|
13
|
+
A silent user failure happens when your code clearly implies that something should happen —
|
|
14
|
+
but from the user’s point of view, nothing meaningful does.
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
|
|
18
|
+
A button click that should navigate… but doesn’t.
|
|
19
|
+
|
|
20
|
+
A form submission that triggers an API call… but shows no feedback.
|
|
21
|
+
|
|
22
|
+
A state update that runs in code… but never reaches the UI.
|
|
23
|
+
|
|
24
|
+
These issues are frustrating for users and difficult for teams to catch.
|
|
25
|
+
|
|
26
|
+
VERAX reads your source code to understand what should happen, then opens your website in a real browser and experiences it like a human user.
|
|
27
|
+
When code expectations and user reality don’t match, VERAX reports the gap — clearly and honestly.
|
|
28
|
+
|
|
29
|
+
VERAX does not guess intent.
|
|
30
|
+
It only reports failures that your code explicitly promises.
|
|
31
|
+
|
|
32
|
+
✅ What VERAX does (today)
|
|
33
|
+
|
|
34
|
+
🔍 Detects silent user failures by comparing code-derived expectations with real browser behavior
|
|
35
|
+
|
|
36
|
+
🧠 Extracts expectations from source code using static analysis:
|
|
37
|
+
|
|
38
|
+
Navigation from HTML links and React Router / Next.js routes
|
|
39
|
+
|
|
40
|
+
Network actions from fetch / axios calls with static URLs
|
|
41
|
+
|
|
42
|
+
State mutations from React useState, Redux dispatch, and Zustand set
|
|
43
|
+
|
|
44
|
+
🖱️ Observes websites like a real user using Playwright (clicks, forms, navigation, scrolling)
|
|
45
|
+
|
|
46
|
+
📊 Assigns confidence levels (HIGH / MEDIUM / LOW) based on evidence strength
|
|
47
|
+
|
|
48
|
+
🧾 Provides clear evidence for every finding:
|
|
49
|
+
|
|
50
|
+
Screenshots
|
|
51
|
+
|
|
52
|
+
Network activity
|
|
53
|
+
|
|
54
|
+
Console errors
|
|
55
|
+
|
|
56
|
+
DOM and state changes
|
|
57
|
+
|
|
58
|
+
💻 Runs as a CLI tool with verax scan and verax flow
|
|
59
|
+
|
|
60
|
+
🧱 Supports real-world projects:
|
|
61
|
+
|
|
62
|
+
Static HTML sites
|
|
63
|
+
|
|
64
|
+
React SPAs
|
|
65
|
+
|
|
66
|
+
Next.js (App Router & Pages Router)
|
|
67
|
+
|
|
68
|
+
🔐 Protects privacy by automatically redacting secrets and sensitive data
|
|
69
|
+
|
|
70
|
+
🚫 What VERAX does NOT do
|
|
71
|
+
|
|
72
|
+
❌ Does not guess intent — no heuristics, no assumptions
|
|
73
|
+
|
|
74
|
+
❌ Does not support dynamic routes (/user/${id} is intentionally skipped)
|
|
75
|
+
|
|
76
|
+
❌ Does not replace QA or tests — it complements them
|
|
77
|
+
|
|
78
|
+
❌ Does not monitor production traffic
|
|
79
|
+
|
|
80
|
+
❌ Does not work for every framework
|
|
81
|
+
|
|
82
|
+
❌ Does not detect every bug — only silent failures backed by code promises
|
|
83
|
+
|
|
84
|
+
❌ Does not use AI — all results are deterministic and explainable
|
|
85
|
+
|
|
86
|
+
🔄 How VERAX works (high-level)
|
|
87
|
+
|
|
88
|
+
VERAX runs three phases automatically:
|
|
89
|
+
|
|
90
|
+
1️⃣ Learn
|
|
91
|
+
|
|
92
|
+
Analyzes your source code to understand:
|
|
93
|
+
|
|
94
|
+
Project structure
|
|
95
|
+
|
|
96
|
+
Routes
|
|
97
|
+
|
|
98
|
+
What interactions promise to do
|
|
99
|
+
Creates a manifest of expectations derived directly from code.
|
|
100
|
+
|
|
101
|
+
2️⃣ Observe
|
|
102
|
+
|
|
103
|
+
Opens your site in a real browser and interacts naturally:
|
|
104
|
+
|
|
105
|
+
Clicks buttons
|
|
106
|
+
|
|
107
|
+
Fills forms
|
|
108
|
+
|
|
109
|
+
Follows links
|
|
110
|
+
Records what actually happens.
|
|
111
|
+
|
|
112
|
+
3️⃣ Detect
|
|
113
|
+
|
|
114
|
+
Compares code expectations with real behavior.
|
|
115
|
+
Reports a finding only when code promised an outcome that did not occur — with evidence and confidence.
|
|
116
|
+
|
|
117
|
+
You run one command. VERAX handles the rest.
|
|
118
|
+
|
|
119
|
+
📦 Installation
|
|
120
|
+
|
|
121
|
+
Requirements: Node.js 18+
|
|
122
|
+
|
|
123
|
+
From npm (when published):
|
|
124
|
+
npm install -g @verax/verax
|
|
125
|
+
|
|
126
|
+
From source:
|
|
127
|
+
git clone <repository-url>
|
|
128
|
+
cd odavlguardian
|
|
129
|
+
npm install
|
|
130
|
+
npm link
|
|
131
|
+
|
|
132
|
+
🚀 Basic usage
|
|
133
|
+
Scan a website:
|
|
134
|
+
verax scan --url <http://localhost:3000>
|
|
135
|
+
|
|
136
|
+
JSON output (CI-friendly):
|
|
137
|
+
verax scan --url <http://localhost:3000> --json
|
|
138
|
+
|
|
139
|
+
Scan a specific project directory:
|
|
140
|
+
verax scan --url <http://localhost:3000> --projectRoot ./my-website
|
|
141
|
+
|
|
142
|
+
Execute a user flow:
|
|
143
|
+
verax flow --flow ./flows/login.json --url <http://localhost:3000>
|
|
144
|
+
|
|
145
|
+
Outputs
|
|
146
|
+
|
|
147
|
+
Artifacts are written to:
|
|
148
|
+
|
|
149
|
+
.verax/runs/<runId>/
|
|
150
|
+
|
|
151
|
+
Includes:
|
|
152
|
+
|
|
153
|
+
summary.json — overall results
|
|
154
|
+
|
|
155
|
+
findings.json — detected issues with evidence
|
|
156
|
+
|
|
157
|
+
traces.jsonl — interaction traces
|
|
158
|
+
|
|
159
|
+
evidence/ — screenshots and logs
|
|
160
|
+
|
|
161
|
+
Exit codes
|
|
162
|
+
|
|
163
|
+
0 — No HIGH confidence findings
|
|
164
|
+
|
|
165
|
+
1 — MEDIUM / LOW findings only
|
|
166
|
+
|
|
167
|
+
2 — At least one HIGH confidence finding
|
|
168
|
+
|
|
169
|
+
3 — Fatal error
|
|
170
|
+
|
|
171
|
+
📊 Understanding results
|
|
172
|
+
|
|
173
|
+
Each finding includes:
|
|
174
|
+
|
|
175
|
+
Type — what kind of failure occurred
|
|
176
|
+
|
|
177
|
+
Reason — plain English explanation
|
|
178
|
+
|
|
179
|
+
Confidence — score (0–100) + level
|
|
180
|
+
|
|
181
|
+
Evidence — screenshots, network data, console logs
|
|
182
|
+
|
|
183
|
+
Confidence levels:
|
|
184
|
+
|
|
185
|
+
HIGH (80–100) — strong, unambiguous evidence
|
|
186
|
+
|
|
187
|
+
MEDIUM (60–79) — likely failure with some ambiguity
|
|
188
|
+
|
|
189
|
+
LOW (<60) — weak or partial evidence
|
|
190
|
+
|
|
191
|
+
VERAX prefers missing an issue over reporting a false one.
|
|
192
|
+
|
|
193
|
+
🔐 Safety and privacy
|
|
194
|
+
|
|
195
|
+
🔒 No guessing
|
|
196
|
+
|
|
197
|
+
🧹 Automatic redaction of secrets and personal data
|
|
198
|
+
|
|
199
|
+
🛑 Safe by default — blocks destructive actions unless explicitly allowed
|
|
200
|
+
|
|
201
|
+
🧭 Flows are opt-in only — nothing runs without your definition
|
|
202
|
+
|
|
203
|
+
🎯 When VERAX is a good fit
|
|
204
|
+
|
|
205
|
+
Marketing and public-facing websites
|
|
206
|
+
|
|
207
|
+
SaaS signup and pricing flows
|
|
208
|
+
|
|
209
|
+
React and Next.js projects
|
|
210
|
+
|
|
211
|
+
CI pipelines that need UX validation
|
|
212
|
+
|
|
213
|
+
Teams that value evidence over assumptions
|
|
214
|
+
|
|
215
|
+
🚫 When VERAX is NOT a good fit
|
|
216
|
+
|
|
217
|
+
Internal admin dashboards
|
|
218
|
+
|
|
219
|
+
Authentication-heavy systems
|
|
220
|
+
|
|
221
|
+
Apps built around highly dynamic routing
|
|
222
|
+
|
|
223
|
+
Unsupported frameworks
|
|
224
|
+
|
|
225
|
+
Teams expecting a full QA replacement
|
|
226
|
+
|
|
227
|
+
🧪 Project status
|
|
228
|
+
|
|
229
|
+
VERAX is a production-grade CLI tool in active development.
|
|
230
|
+
It is designed for early adopters and technical teams.
|
|
231
|
+
|
|
232
|
+
VERAX is not a SaaS product.
|
|
233
|
+
It runs locally or in CI. There is no hosted service.
|
|
234
|
+
|
|
235
|
+
📄 License
|
|
236
|
+
|
|
237
|
+
MIT
|
package/bin/verax.js
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname, resolve } from 'path';
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'fs';
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
import { learn } from '../src/verax/index.js';
|
|
8
|
+
import { resolveWorkspaceRoot } from '../src/verax/resolve-workspace-root.js';
|
|
9
|
+
import { initArtifactPaths, writeSummary, writeFindings, appendTrace } from '../src/verax/shared/artifact-manager.js';
|
|
10
|
+
import { redactFinding, redactTrace } from '../src/verax/shared/redaction.js';
|
|
11
|
+
import { initMetrics, recordMetric, getMetrics } from '../src/verax/shared/timing-metrics.js';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const command = args[0];
|
|
19
|
+
|
|
20
|
+
// Handle Wave 9 CLI commands
|
|
21
|
+
if (command === 'scan') {
|
|
22
|
+
await handleScan(args.slice(1));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (command === 'flow') {
|
|
27
|
+
await handleFlow(args.slice(1));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (['--help', '-h', 'help'].includes(command)) {
|
|
32
|
+
printHelp();
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Default: interactive mode (existing behavior)
|
|
37
|
+
console.log('VERAX\n');
|
|
38
|
+
|
|
39
|
+
// Reuse parsed args from above for optional flags
|
|
40
|
+
let projectDir = null;
|
|
41
|
+
let url = null;
|
|
42
|
+
let manifestPath = null;
|
|
43
|
+
|
|
44
|
+
const projectDirIndex = args.indexOf('--project-dir');
|
|
45
|
+
const projectDirArg = projectDirIndex !== -1 && args[projectDirIndex + 1] ? args[projectDirIndex + 1] : null;
|
|
46
|
+
|
|
47
|
+
const urlIndex = args.indexOf('--url');
|
|
48
|
+
if (urlIndex !== -1 && urlIndex + 1 < args.length) {
|
|
49
|
+
url = args[urlIndex + 1];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const manifestIndex = args.indexOf('--manifest');
|
|
53
|
+
if (manifestIndex !== -1 && manifestIndex + 1 < args.length) {
|
|
54
|
+
manifestPath = resolve(args[manifestIndex + 1]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Resolve workspace root using the new function
|
|
58
|
+
try {
|
|
59
|
+
const resolved = resolveWorkspaceRoot(projectDirArg, process.cwd());
|
|
60
|
+
projectDir = resolved.workspaceRoot;
|
|
61
|
+
|
|
62
|
+
if (resolved.isRepoRoot) {
|
|
63
|
+
console.error(
|
|
64
|
+
'VERAX: Refusing to write artifacts in repository root.\n' +
|
|
65
|
+
'Use --project-dir to specify the target project directory.'
|
|
66
|
+
);
|
|
67
|
+
process.exit(2);
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error(`Error: ${error.message}`);
|
|
71
|
+
process.exit(2);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const actions = ['Scan my website'];
|
|
75
|
+
let action;
|
|
76
|
+
|
|
77
|
+
if (actions.length === 1) {
|
|
78
|
+
action = actions[0];
|
|
79
|
+
} else {
|
|
80
|
+
const result = await inquirer.prompt([
|
|
81
|
+
{
|
|
82
|
+
type: 'list',
|
|
83
|
+
name: 'action',
|
|
84
|
+
message: 'What would you like to do?',
|
|
85
|
+
choices: actions,
|
|
86
|
+
default: actions[0]
|
|
87
|
+
}
|
|
88
|
+
]);
|
|
89
|
+
action = result.action;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (action === 'Scan my website') {
|
|
93
|
+
try {
|
|
94
|
+
console.log('Understanding your website...');
|
|
95
|
+
const manifest = await learn(projectDir);
|
|
96
|
+
|
|
97
|
+
console.log(`\nProject type: ${manifest.projectType}`);
|
|
98
|
+
console.log(`Total routes: ${manifest.routes.length}`);
|
|
99
|
+
console.log(`Public routes: ${manifest.publicRoutes.length}`);
|
|
100
|
+
console.log(`Internal routes: ${manifest.internalRoutes.length}`);
|
|
101
|
+
|
|
102
|
+
if (url) {
|
|
103
|
+
const { scan } = await import('../src/verax/index.js');
|
|
104
|
+
const result = await scan(projectDir, url, manifestPath);
|
|
105
|
+
const { observation, findings, scanSummary } = result;
|
|
106
|
+
|
|
107
|
+
console.log('\nObserving real user interactions...');
|
|
108
|
+
console.log('Comparing expectations with reality...');
|
|
109
|
+
|
|
110
|
+
console.log('\nScan complete.\n');
|
|
111
|
+
console.log(`Total interactions observed: ${observation.traces.length}`);
|
|
112
|
+
console.log(`Silent failures detected: ${findings.findings.length}`);
|
|
113
|
+
|
|
114
|
+
if (findings.findings.length > 0) {
|
|
115
|
+
console.log(`\nFindings report: ${findings.findingsPath}\n`);
|
|
116
|
+
} else {
|
|
117
|
+
console.log('\nNo silent user failures were detected.');
|
|
118
|
+
console.log(`\nFindings report: ${findings.findingsPath}\n`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (scanSummary) {
|
|
122
|
+
const truth = scanSummary.truth;
|
|
123
|
+
console.log('Truth Summary:');
|
|
124
|
+
console.log(`- Learn: routes=${truth.learn.routesDiscovered} (confidence: ${truth.learn.routesConfidence}, source: ${truth.learn.routesSource}), expectations=${truth.learn.expectationsDiscovered} (strong=${truth.learn.expectationsStrong}, weak=${truth.learn.expectationsWeak})`);
|
|
125
|
+
if (truth.learn.validation) {
|
|
126
|
+
console.log(`- Learn validation: validated=${truth.learn.validation.routesValidated}, reachable=${truth.learn.validation.routesReachable}, unreachable=${truth.learn.validation.routesUnreachable}`);
|
|
127
|
+
}
|
|
128
|
+
const coverage = truth.observe.coverage;
|
|
129
|
+
const coverageLine = coverage
|
|
130
|
+
? `Coverage: selected=${coverage.candidatesSelected}/${coverage.candidatesDiscovered} (cap=${coverage.cap})${coverage.capped ? ' — capped' : ''}`
|
|
131
|
+
: null;
|
|
132
|
+
console.log(`- Observe: interactions=${truth.observe.interactionsObserved}, external-blocked=${truth.observe.externalNavigationBlockedCount}, timeouts=${truth.observe.timeoutsCount}`);
|
|
133
|
+
if (coverageLine) {
|
|
134
|
+
console.log(` - ${coverageLine}`);
|
|
135
|
+
}
|
|
136
|
+
console.log(`- Detect: analyzed=${truth.detect.interactionsAnalyzed}, skipped(no expectation)=${truth.detect.interactionsSkippedNoExpectation}, findings=${truth.detect.findingsCount}`);
|
|
137
|
+
if (truth.detect.skips && truth.detect.skips.total > 0) {
|
|
138
|
+
const topReasons = truth.detect.skips.reasons.slice(0, 3).map(r => `${r.code}=${r.count}`).join(', ');
|
|
139
|
+
console.log(`- Detect skips: ${truth.detect.skips.total} (top: ${topReasons})`);
|
|
140
|
+
}
|
|
141
|
+
console.log(`- Scan summary: ${scanSummary.summaryPath}\n`);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
console.log('\nNote: Provide --url to observe website interactions\n');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
process.exit(0);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error(`\nError: ${error.message}`);
|
|
150
|
+
if (error.stack && process.env.DEBUG) {
|
|
151
|
+
console.error(error.stack);
|
|
152
|
+
}
|
|
153
|
+
process.exit(2);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Parse command-line arguments into key-value object
|
|
160
|
+
*/
|
|
161
|
+
function parseArgs(argArray) {
|
|
162
|
+
const opts = {};
|
|
163
|
+
for (let i = 0; i < argArray.length; i++) {
|
|
164
|
+
if (argArray[i].startsWith('--')) {
|
|
165
|
+
const key = argArray[i].substring(2);
|
|
166
|
+
if (i + 1 < argArray.length && !argArray[i + 1].startsWith('--')) {
|
|
167
|
+
opts[key] = argArray[i + 1];
|
|
168
|
+
i++;
|
|
169
|
+
} else {
|
|
170
|
+
opts[key] = true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return opts;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Print help message
|
|
179
|
+
*/
|
|
180
|
+
function printHelp() {
|
|
181
|
+
console.log(`
|
|
182
|
+
VERAX — Web Application Verification Platform
|
|
183
|
+
|
|
184
|
+
Usage:
|
|
185
|
+
verax [interactive] Interactive mode
|
|
186
|
+
verax scan --url <url> [--projectRoot <path>] [--json] [--out <dir>]
|
|
187
|
+
verax flow --flow <path> [--url <url>] [--json] [--out <dir>]
|
|
188
|
+
verax --help Show this message
|
|
189
|
+
|
|
190
|
+
Commands:
|
|
191
|
+
scan Run full scan on a live URL
|
|
192
|
+
flow Execute a user flow against a URL
|
|
193
|
+
|
|
194
|
+
Options:
|
|
195
|
+
--url <url> Target URL
|
|
196
|
+
--flow <path> Path to flow definition file
|
|
197
|
+
--projectRoot <p> Project root directory (defaults to cwd)
|
|
198
|
+
--json Output machine-readable JSON
|
|
199
|
+
--out <dir> Output directory (defaults to .verax/runs)
|
|
200
|
+
--help, -h Show this help
|
|
201
|
+
|
|
202
|
+
Exit Codes:
|
|
203
|
+
0 No HIGH findings
|
|
204
|
+
1 MEDIUM/LOW findings only
|
|
205
|
+
2 At least one HIGH finding
|
|
206
|
+
3 Fatal error
|
|
207
|
+
`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Handle 'verax scan' command - integrated with main pipeline
|
|
212
|
+
*/
|
|
213
|
+
async function handleScan(scanArgs) {
|
|
214
|
+
const opts = parseArgs(scanArgs);
|
|
215
|
+
|
|
216
|
+
if (!opts.url) {
|
|
217
|
+
console.error('Error: --url is required');
|
|
218
|
+
process.exit(3);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const projectRoot = opts.projectRoot || process.cwd();
|
|
222
|
+
const url = opts.url;
|
|
223
|
+
const jsonOutput = opts.json === true || opts.json === 'true';
|
|
224
|
+
const outDir = opts.out;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
initMetrics();
|
|
228
|
+
const startTime = Date.now();
|
|
229
|
+
|
|
230
|
+
// Initialize artifact paths
|
|
231
|
+
const artifactPaths = outDir
|
|
232
|
+
? { runDir: outDir, summary: `${outDir}/summary.json`, findings: `${outDir}/findings.json`, traces: `${outDir}/traces.jsonl` }
|
|
233
|
+
: initArtifactPaths(projectRoot);
|
|
234
|
+
|
|
235
|
+
// Ensure artifact directories exist
|
|
236
|
+
mkdirSync(artifactPaths.runDir, { recursive: true });
|
|
237
|
+
|
|
238
|
+
// Run the full VERAX pipeline using the scan() orchestrator
|
|
239
|
+
console.error(`[VERAX] Starting scan for ${url}`);
|
|
240
|
+
const resolvedRootResult = await resolveWorkspaceRoot(projectRoot);
|
|
241
|
+
const resolvedRoot = resolvedRootResult.workspaceRoot;
|
|
242
|
+
|
|
243
|
+
const scanStart = Date.now();
|
|
244
|
+
const { scan } = await import('../src/verax/index.js');
|
|
245
|
+
const result = await scan(resolvedRoot, url, null, artifactPaths.runId);
|
|
246
|
+
recordMetric('totalMs', Date.now() - scanStart);
|
|
247
|
+
|
|
248
|
+
const manifest = result.manifest;
|
|
249
|
+
const observation = result.observation;
|
|
250
|
+
const findings = result.findings;
|
|
251
|
+
|
|
252
|
+
const metrics = getMetrics();
|
|
253
|
+
metrics.totalMs = Date.now() - startTime;
|
|
254
|
+
|
|
255
|
+
// Count findings by level
|
|
256
|
+
const findingsList = findings?.findings || [];
|
|
257
|
+
const findingsCounts = {
|
|
258
|
+
HIGH: findingsList.filter(f => f.confidence?.level === 'HIGH').length,
|
|
259
|
+
MEDIUM: findingsList.filter(f => f.confidence?.level === 'MEDIUM').length,
|
|
260
|
+
LOW: findingsList.filter(f => f.confidence?.level === 'LOW').length,
|
|
261
|
+
UNKNOWN: findingsList.filter(f => !f.confidence?.level || f.confidence?.level === 'UNKNOWN').length
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Get top findings
|
|
265
|
+
const topFindings = findingsList
|
|
266
|
+
.slice(0, 3)
|
|
267
|
+
.map(f => ({
|
|
268
|
+
type: f.type,
|
|
269
|
+
reason: f.reason,
|
|
270
|
+
confidence: f.confidence?.score || 0
|
|
271
|
+
}));
|
|
272
|
+
|
|
273
|
+
// Write summary with metrics to artifact paths
|
|
274
|
+
writeSummary(artifactPaths, {
|
|
275
|
+
url,
|
|
276
|
+
projectRoot: resolvedRoot,
|
|
277
|
+
metrics,
|
|
278
|
+
findingsCounts,
|
|
279
|
+
topFindings
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Determine exit code
|
|
283
|
+
const HIGH_COUNT = findingsCounts.HIGH;
|
|
284
|
+
const hasAny = Object.values(findingsCounts).reduce((a, b) => a + b, 0) > 0;
|
|
285
|
+
|
|
286
|
+
if (jsonOutput) {
|
|
287
|
+
const summary = {
|
|
288
|
+
runId: artifactPaths.runId,
|
|
289
|
+
url,
|
|
290
|
+
metrics,
|
|
291
|
+
findingsCounts,
|
|
292
|
+
topFindings,
|
|
293
|
+
total: findingsList.length
|
|
294
|
+
};
|
|
295
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
296
|
+
} else {
|
|
297
|
+
console.error(`[VERAX] Scan complete in ${(metrics.totalMs / 1000).toFixed(2)}s`);
|
|
298
|
+
console.error(`[VERAX] Findings: HIGH=${HIGH_COUNT}, MEDIUM=${findingsCounts.MEDIUM}, LOW=${findingsCounts.LOW}`);
|
|
299
|
+
if (topFindings.length > 0) {
|
|
300
|
+
console.error(`[VERAX] Top findings:`);
|
|
301
|
+
topFindings.forEach(f => {
|
|
302
|
+
console.error(` - [${f.confidence}%] ${f.type}: ${f.reason}`);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Exit with appropriate code
|
|
308
|
+
if (HIGH_COUNT > 0) {
|
|
309
|
+
process.exit(2);
|
|
310
|
+
} else if (hasAny) {
|
|
311
|
+
process.exit(1);
|
|
312
|
+
} else {
|
|
313
|
+
process.exit(0);
|
|
314
|
+
}
|
|
315
|
+
} catch (error) {
|
|
316
|
+
if (!jsonOutput) {
|
|
317
|
+
console.error(`[VERAX] Scan failed: ${error.message}`);
|
|
318
|
+
}
|
|
319
|
+
process.exit(3);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Handle 'verax flow' command - execute a user flow
|
|
325
|
+
*/
|
|
326
|
+
async function handleFlow(flowArgs) {
|
|
327
|
+
const opts = parseArgs(flowArgs);
|
|
328
|
+
|
|
329
|
+
if (!opts.flow) {
|
|
330
|
+
console.error('Error: --flow is required');
|
|
331
|
+
process.exit(3);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const flowPath = resolve(opts.flow);
|
|
335
|
+
if (!existsSync(flowPath)) {
|
|
336
|
+
console.error(`Error: Flow file not found: ${flowPath}`);
|
|
337
|
+
process.exit(3);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const url = opts.url;
|
|
341
|
+
|
|
342
|
+
const jsonOutput = opts.json === true || opts.json === 'true';
|
|
343
|
+
const projectRoot = opts.projectRoot || process.cwd();
|
|
344
|
+
const outDir = opts.out;
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
initMetrics();
|
|
348
|
+
const startTime = Date.now();
|
|
349
|
+
|
|
350
|
+
// Initialize artifact paths
|
|
351
|
+
const artifactPaths = outDir
|
|
352
|
+
? { runDir: outDir, flows: `${outDir}/flows` }
|
|
353
|
+
: initArtifactPaths(projectRoot);
|
|
354
|
+
mkdirSync(artifactPaths.runDir, { recursive: true });
|
|
355
|
+
if (artifactPaths.flows) {
|
|
356
|
+
mkdirSync(artifactPaths.flows, { recursive: true });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Load and validate flow spec
|
|
360
|
+
const { readFileSync } = await import('fs');
|
|
361
|
+
const flowContent = readFileSync(flowPath, 'utf-8');
|
|
362
|
+
const flowSpec = JSON.parse(flowContent);
|
|
363
|
+
|
|
364
|
+
// Override baseUrl with --url if provided
|
|
365
|
+
if (url) {
|
|
366
|
+
flowSpec.baseUrl = url;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const { validateFlowSpec } = await import('../src/verax/flow/flow-spec.js');
|
|
370
|
+
const validatedSpec = validateFlowSpec(flowSpec);
|
|
371
|
+
|
|
372
|
+
// Use baseUrl from spec for navigation
|
|
373
|
+
const targetUrl = validatedSpec.baseUrl;
|
|
374
|
+
|
|
375
|
+
// Create browser and sensors
|
|
376
|
+
const { createBrowser, navigateToUrl, closeBrowser } = await import('../src/verax/observe/browser.js');
|
|
377
|
+
const { NetworkSensor } = await import('../src/verax/observe/network-sensor.js');
|
|
378
|
+
const { ConsoleSensor } = await import('../src/verax/observe/console-sensor.js');
|
|
379
|
+
const { UISignalSensor } = await import('../src/verax/observe/ui-signal-sensor.js');
|
|
380
|
+
|
|
381
|
+
const { browser, page } = await createBrowser();
|
|
382
|
+
const sensors = {
|
|
383
|
+
network: new NetworkSensor(),
|
|
384
|
+
console: new ConsoleSensor(),
|
|
385
|
+
uiSignals: new UISignalSensor()
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
await navigateToUrl(page, targetUrl);
|
|
390
|
+
|
|
391
|
+
// Execute flow
|
|
392
|
+
const { executeFlow } = await import('../src/verax/flow/flow-engine.js');
|
|
393
|
+
const flowResult = await executeFlow(page, validatedSpec, sensors);
|
|
394
|
+
|
|
395
|
+
await closeBrowser(browser);
|
|
396
|
+
|
|
397
|
+
recordMetric('totalMs', Date.now() - startTime);
|
|
398
|
+
const metrics = getMetrics();
|
|
399
|
+
|
|
400
|
+
// Write flow results
|
|
401
|
+
const flowResultsPath = resolve(artifactPaths.flows || artifactPaths.runDir, 'flow-results.json');
|
|
402
|
+
writeFileSync(flowResultsPath, JSON.stringify({
|
|
403
|
+
flow: validatedSpec.name,
|
|
404
|
+
url: targetUrl,
|
|
405
|
+
success: flowResult.success,
|
|
406
|
+
findings: flowResult.findings,
|
|
407
|
+
stepResults: flowResult.stepResults,
|
|
408
|
+
metrics
|
|
409
|
+
}, null, 2) + '\n');
|
|
410
|
+
|
|
411
|
+
if (jsonOutput) {
|
|
412
|
+
console.log(JSON.stringify({
|
|
413
|
+
flow: validatedSpec.name,
|
|
414
|
+
url: targetUrl,
|
|
415
|
+
success: flowResult.success,
|
|
416
|
+
findingsCount: flowResult.findings.length,
|
|
417
|
+
metrics
|
|
418
|
+
}, null, 2));
|
|
419
|
+
} else {
|
|
420
|
+
console.error(`[VERAX] Flow execution complete in ${(metrics.totalMs / 1000).toFixed(2)}s`);
|
|
421
|
+
console.error(`[VERAX] Flow: ${validatedSpec.name}`);
|
|
422
|
+
console.error(`[VERAX] Success: ${flowResult.success}`);
|
|
423
|
+
console.error(`[VERAX] Findings: ${flowResult.findings.length}`);
|
|
424
|
+
if (flowResult.findings.length > 0) {
|
|
425
|
+
flowResult.findings.forEach((f, i) => {
|
|
426
|
+
console.error(` ${i + 1}. [Step ${f.stepIndex}] ${f.type}: ${f.reason}`);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Exit code: 0 if success, 2 if findings, 3 if error
|
|
432
|
+
if (!flowResult.success) {
|
|
433
|
+
process.exit(2);
|
|
434
|
+
} else {
|
|
435
|
+
process.exit(0);
|
|
436
|
+
}
|
|
437
|
+
} catch (error) {
|
|
438
|
+
await closeBrowser(browser);
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
} catch (error) {
|
|
442
|
+
if (!jsonOutput) {
|
|
443
|
+
console.error(`[VERAX] Flow execution failed: ${error.message}`);
|
|
444
|
+
}
|
|
445
|
+
process.exit(3);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
main().catch((error) => {
|
|
450
|
+
console.error(`Fatal error: ${error.message}`);
|
|
451
|
+
process.exit(2);
|
|
452
|
+
});
|