@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
|
@@ -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
|
+
}
|