@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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +237 -0
  3. package/bin/verax.js +452 -0
  4. package/package.json +57 -0
  5. package/src/verax/detect/comparison.js +69 -0
  6. package/src/verax/detect/confidence-engine.js +498 -0
  7. package/src/verax/detect/evidence-validator.js +33 -0
  8. package/src/verax/detect/expectation-model.js +204 -0
  9. package/src/verax/detect/findings-writer.js +31 -0
  10. package/src/verax/detect/index.js +397 -0
  11. package/src/verax/detect/skip-classifier.js +202 -0
  12. package/src/verax/flow/flow-engine.js +265 -0
  13. package/src/verax/flow/flow-spec.js +145 -0
  14. package/src/verax/flow/redaction.js +74 -0
  15. package/src/verax/index.js +97 -0
  16. package/src/verax/learn/action-contract-extractor.js +281 -0
  17. package/src/verax/learn/ast-contract-extractor.js +255 -0
  18. package/src/verax/learn/index.js +18 -0
  19. package/src/verax/learn/manifest-writer.js +97 -0
  20. package/src/verax/learn/project-detector.js +87 -0
  21. package/src/verax/learn/react-router-extractor.js +73 -0
  22. package/src/verax/learn/route-extractor.js +122 -0
  23. package/src/verax/learn/route-validator.js +215 -0
  24. package/src/verax/learn/source-instrumenter.js +214 -0
  25. package/src/verax/learn/static-extractor.js +222 -0
  26. package/src/verax/learn/truth-assessor.js +96 -0
  27. package/src/verax/learn/ts-contract-resolver.js +395 -0
  28. package/src/verax/observe/browser.js +22 -0
  29. package/src/verax/observe/console-sensor.js +166 -0
  30. package/src/verax/observe/dom-signature.js +23 -0
  31. package/src/verax/observe/domain-boundary.js +38 -0
  32. package/src/verax/observe/evidence-capture.js +5 -0
  33. package/src/verax/observe/human-driver.js +376 -0
  34. package/src/verax/observe/index.js +67 -0
  35. package/src/verax/observe/interaction-discovery.js +269 -0
  36. package/src/verax/observe/interaction-runner.js +410 -0
  37. package/src/verax/observe/network-sensor.js +173 -0
  38. package/src/verax/observe/selector-generator.js +74 -0
  39. package/src/verax/observe/settle.js +155 -0
  40. package/src/verax/observe/state-ui-sensor.js +200 -0
  41. package/src/verax/observe/traces-writer.js +82 -0
  42. package/src/verax/observe/ui-signal-sensor.js +197 -0
  43. package/src/verax/resolve-workspace-root.js +173 -0
  44. package/src/verax/scan-summary-writer.js +41 -0
  45. package/src/verax/shared/artifact-manager.js +139 -0
  46. package/src/verax/shared/caching.js +104 -0
  47. package/src/verax/shared/expectation-proof.js +4 -0
  48. package/src/verax/shared/redaction.js +227 -0
  49. package/src/verax/shared/retry-policy.js +89 -0
  50. package/src/verax/shared/timing-metrics.js +44 -0
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Wave 9 — Privacy & Redaction Expansion
3
+ *
4
+ * Redacts sensitive information from all artifacts:
5
+ * - Authorization headers (Authorization, Cookie, X-Auth-Token, X-API-Key)
6
+ * - Bearer tokens (pattern: /bearer\s+\S+/i)
7
+ * - Query parameters (token, auth, session, key, apikey, secret)
8
+ * - localStorage/sessionStorage values
9
+ * - Request/response bodies containing sensitive keys
10
+ *
11
+ * Applied to: network logs, screenshots, traces, findings
12
+ */
13
+
14
+ const SENSITIVE_HEADERS = [
15
+ 'authorization',
16
+ 'cookie',
17
+ 'x-auth-token',
18
+ 'x-api-key',
19
+ 'api-key',
20
+ 'x-token',
21
+ 'x-session-token',
22
+ 'set-cookie'
23
+ ];
24
+
25
+ const SENSITIVE_QUERY_PARAMS = [
26
+ 'token',
27
+ 'auth',
28
+ 'session',
29
+ 'key',
30
+ 'apikey',
31
+ 'api_key',
32
+ 'secret',
33
+ 'password',
34
+ 'pwd',
35
+ 'access_token',
36
+ 'refresh_token',
37
+ 'auth_token'
38
+ ];
39
+
40
+ const BEARER_TOKEN_PATTERN = /bearer\s+\S+/gi;
41
+
42
+ /**
43
+ * Redact headers object (from network request/response).
44
+ * @param {Object} headers - Headers object
45
+ * @returns {Object} - Redacted headers
46
+ */
47
+ export function redactHeaders(headers) {
48
+ if (!headers || typeof headers !== 'object') return headers;
49
+
50
+ const redacted = { ...headers };
51
+ for (const key of Object.keys(redacted)) {
52
+ if (SENSITIVE_HEADERS.includes(key.toLowerCase())) {
53
+ redacted[key] = '[REDACTED]';
54
+ }
55
+ }
56
+ return redacted;
57
+ }
58
+
59
+ /**
60
+ * Redact query parameters in a URL string.
61
+ * @param {string} url - Full URL
62
+ * @returns {string} - URL with sensitive query params redacted
63
+ */
64
+ export function redactQueryParams(url) {
65
+ if (!url || typeof url !== 'string') return url;
66
+
67
+ try {
68
+ const urlObj = new URL(url);
69
+ for (const key of SENSITIVE_QUERY_PARAMS) {
70
+ if (urlObj.searchParams.has(key)) {
71
+ urlObj.searchParams.set(key, '[REDACTED]');
72
+ }
73
+ }
74
+ return urlObj.toString();
75
+ } catch (e) {
76
+ // If URL parsing fails, try regex approach
77
+ return redactBearerTokens(url);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Redact bearer tokens from any string.
83
+ * @param {string} text - Text possibly containing bearer tokens
84
+ * @returns {string} - Redacted text
85
+ */
86
+ export function redactBearerTokens(text) {
87
+ if (!text || typeof text !== 'string') return text;
88
+ return text.replace(BEARER_TOKEN_PATTERN, 'Bearer [REDACTED]');
89
+ }
90
+
91
+ /**
92
+ * Redact localStorage/sessionStorage values.
93
+ * @param {Object} storage - Storage object { key: value }
94
+ * @returns {Object} - Redacted storage
95
+ */
96
+ export function redactStorage(storage) {
97
+ if (!storage || typeof storage !== 'object') return storage;
98
+
99
+ const redacted = { ...storage };
100
+ for (const key of Object.keys(redacted)) {
101
+ if (isSensitiveStorageKey(key)) {
102
+ redacted[key] = '[REDACTED]';
103
+ }
104
+ }
105
+ return redacted;
106
+ }
107
+
108
+ /**
109
+ * Check if a storage key should be redacted.
110
+ * @param {string} key - Storage key
111
+ * @returns {boolean}
112
+ */
113
+ function isSensitiveStorageKey(key) {
114
+ const lower = key.toLowerCase();
115
+ return SENSITIVE_QUERY_PARAMS.some(sensitive => lower.includes(sensitive));
116
+ }
117
+
118
+ /**
119
+ * Redact request body (if it's JSON).
120
+ * @param {*} body - Request body (string or object)
121
+ * @returns {*} - Redacted body
122
+ */
123
+ export function redactRequestBody(body) {
124
+ if (!body) return body;
125
+
126
+ try {
127
+ let obj = typeof body === 'string' ? JSON.parse(body) : body;
128
+ obj = redactSensitiveFields(obj);
129
+ return typeof body === 'string' ? JSON.stringify(obj) : obj;
130
+ } catch (e) {
131
+ // If not JSON, apply string redaction
132
+ return redactBearerTokens(String(body));
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Deep redact sensitive fields in any object.
138
+ * @param {*} obj - Object to redact
139
+ * @returns {*} - Redacted object
140
+ */
141
+ export function redactSensitiveFields(obj) {
142
+ if (obj === null || obj === undefined) return obj;
143
+ if (typeof obj !== 'object') return obj;
144
+
145
+ if (Array.isArray(obj)) {
146
+ return obj.map(item => redactSensitiveFields(item));
147
+ }
148
+
149
+ const redacted = {};
150
+ for (const [key, value] of Object.entries(obj)) {
151
+ if (isSensitiveStorageKey(key)) {
152
+ redacted[key] = '[REDACTED]';
153
+ } else if (typeof value === 'object') {
154
+ redacted[key] = redactSensitiveFields(value);
155
+ } else if (typeof value === 'string') {
156
+ redacted[key] = redactBearerTokens(value);
157
+ } else {
158
+ redacted[key] = value;
159
+ }
160
+ }
161
+ return redacted;
162
+ }
163
+
164
+ /**
165
+ * Redact an entire network log entry.
166
+ * @param {Object} log - Network log entry
167
+ * @returns {Object} - Redacted log
168
+ */
169
+ export function redactNetworkLog(log) {
170
+ if (!log || typeof log !== 'object') return log;
171
+
172
+ return {
173
+ ...log,
174
+ url: redactQueryParams(log.url),
175
+ requestHeaders: redactHeaders(log.requestHeaders),
176
+ responseHeaders: redactHeaders(log.responseHeaders),
177
+ requestBody: redactRequestBody(log.requestBody),
178
+ responseBody: redactRequestBody(log.responseBody)
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Redact an entire finding object.
184
+ * @param {Object} finding - Finding object
185
+ * @returns {Object} - Redacted finding
186
+ */
187
+ export function redactFinding(finding) {
188
+ if (!finding || typeof finding !== 'object') return finding;
189
+
190
+ const redacted = { ...finding };
191
+ if (redacted.evidence && typeof redacted.evidence === 'object') {
192
+ // Deep redact all sensitive fields in evidence
193
+ const redactedEvidence = { ...redacted.evidence };
194
+ if (redactedEvidence.url) {
195
+ redactedEvidence.url = redactQueryParams(redactedEvidence.url);
196
+ }
197
+ if (redactedEvidence.headers) {
198
+ redactedEvidence.headers = redactHeaders(redactedEvidence.headers);
199
+ }
200
+ redacted.evidence = redactSensitiveFields(redactedEvidence);
201
+ }
202
+ if (redacted.url) {
203
+ redacted.url = redactQueryParams(redacted.url);
204
+ }
205
+ return redacted;
206
+ }
207
+
208
+ /**
209
+ * Redact an entire trace object.
210
+ * @param {Object} trace - Trace object
211
+ * @returns {Object} - Redacted trace
212
+ */
213
+ export function redactTrace(trace) {
214
+ if (!trace || typeof trace !== 'object') return trace;
215
+
216
+ const redacted = { ...trace };
217
+ if (redacted.url) {
218
+ redacted.url = redactQueryParams(redacted.url);
219
+ }
220
+ if (redacted.network && Array.isArray(redacted.network)) {
221
+ redacted.network = redacted.network.map(redactNetworkLog);
222
+ }
223
+ if (redacted.interaction) {
224
+ redacted.interaction = redactSensitiveFields(redacted.interaction);
225
+ }
226
+ return redacted;
227
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Wave 9 — Deterministic Retry Policy
3
+ *
4
+ * Provides retry logic for navigation and interaction operations that may fail
5
+ * due to element detachment or asynchronous settling issues.
6
+ *
7
+ * Rules:
8
+ * - Max 2 retries (3 total attempts)
9
+ * - Backoff: 200ms, then 400ms
10
+ * - Recorded in attempt.meta.retriesUsed
11
+ * - Deterministic: same failures don't retry
12
+ */
13
+
14
+ const MAX_RETRIES = 2;
15
+ const RETRY_DELAYS = [200, 400]; // ms
16
+
17
+ /**
18
+ * Determine if an error is retryable (e.g., element detached, clickability).
19
+ * @param {Error} error - The error that occurred
20
+ * @returns {boolean} - True if we should retry
21
+ */
22
+ export function isRetryableError(error) {
23
+ if (!error) return false;
24
+
25
+ const message = error.message || '';
26
+
27
+ // Element detachment/clickability errors from Playwright/Puppeteer
28
+ const retryablePatterns = [
29
+ 'element is not attached to the DOM',
30
+ 'element is not visible',
31
+ 'element is not clickable',
32
+ 'element was detached from the DOM',
33
+ 'timeout waiting for element',
34
+ 'Navigation failed',
35
+ 'net::ERR_' // Network timeouts
36
+ ];
37
+
38
+ return retryablePatterns.some(pattern => message.includes(pattern));
39
+ }
40
+
41
+ /**
42
+ * Retry an operation with exponential backoff.
43
+ * @param {Function} fn - Async function to retry
44
+ * @param {string} operationName - For logging
45
+ * @returns {Promise<{result: *, retriesUsed: number}>}
46
+ */
47
+ export async function retryOperation(fn, operationName = 'operation') {
48
+ let lastError = null;
49
+ let retriesUsed = 0;
50
+
51
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
52
+ try {
53
+ const result = await fn();
54
+ return { result, retriesUsed };
55
+ } catch (error) {
56
+ lastError = error;
57
+
58
+ // Check if we should retry
59
+ if (attempt < MAX_RETRIES && isRetryableError(error)) {
60
+ retriesUsed++;
61
+ const delayMs = RETRY_DELAYS[attempt];
62
+ await new Promise(resolve => setTimeout(resolve, delayMs));
63
+ // Continue to next attempt
64
+ } else {
65
+ // Don't retry: either out of retries or non-retryable error
66
+ throw error;
67
+ }
68
+ }
69
+ }
70
+
71
+ throw lastError;
72
+ }
73
+
74
+ /**
75
+ * Create a retryable version of an async function.
76
+ * @param {Function} fn - Async function
77
+ * @param {string} opName - Operation name for logging
78
+ * @returns {Function} - Wrapped function that retries automatically
79
+ */
80
+ export function makeRetryable(fn, opName = 'op') {
81
+ return async function(...args) {
82
+ const { result, retriesUsed } = await retryOperation(
83
+ () => fn(...args),
84
+ opName
85
+ );
86
+ // Return both result and metadata about retries
87
+ return { result, meta: { retriesUsed } };
88
+ };
89
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Wave 9 — Timing Metrics Collector
3
+ *
4
+ * Collects performance metrics throughout the scan pipeline.
5
+ * Metrics are stored in-memory and included in final artifacts.
6
+ *
7
+ * Tracked phases:
8
+ * - parseMs: Time to parse/load page
9
+ * - resolveMs: Time to resolve TS contracts
10
+ * - observeMs: Time to observe interactions
11
+ * - detectMs: Time to detect findings
12
+ * - totalMs: Total scan duration
13
+ */
14
+
15
+ const metrics = {};
16
+ let startTime = null;
17
+
18
+ export function initMetrics() {
19
+ metrics.start = Date.now();
20
+ startTime = Date.now();
21
+ }
22
+
23
+ export function recordMetric(phase, durationMs) {
24
+ if (!metrics[phase]) {
25
+ metrics[phase] = 0;
26
+ }
27
+ metrics[phase] += durationMs;
28
+ }
29
+
30
+ export function getMetrics() {
31
+ const now = Date.now();
32
+ return {
33
+ parseMs: metrics.parseMs || 0,
34
+ resolveMs: metrics.resolveMs || 0,
35
+ observeMs: metrics.observeMs || 0,
36
+ detectMs: metrics.detectMs || 0,
37
+ totalMs: startTime ? now - startTime : 0
38
+ };
39
+ }
40
+
41
+ export function clearMetrics() {
42
+ Object.keys(metrics).forEach(k => delete metrics[k]);
43
+ startTime = null;
44
+ }