har-o-scope 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 (75) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +179 -0
  3. package/completions/har-o-scope.bash +64 -0
  4. package/completions/har-o-scope.fish +43 -0
  5. package/completions/har-o-scope.zsh +63 -0
  6. package/dist/cli/colors.d.ts +17 -0
  7. package/dist/cli/colors.d.ts.map +1 -0
  8. package/dist/cli/colors.js +54 -0
  9. package/dist/cli/demo.d.ts +7 -0
  10. package/dist/cli/demo.d.ts.map +1 -0
  11. package/dist/cli/demo.js +62 -0
  12. package/dist/cli/formatters.d.ts +12 -0
  13. package/dist/cli/formatters.d.ts.map +1 -0
  14. package/dist/cli/formatters.js +249 -0
  15. package/dist/cli/index.d.ts +3 -0
  16. package/dist/cli/index.d.ts.map +1 -0
  17. package/dist/cli/index.js +260 -0
  18. package/dist/cli/rules.d.ts +3 -0
  19. package/dist/cli/rules.d.ts.map +1 -0
  20. package/dist/cli/rules.js +36 -0
  21. package/dist/cli/sarif.d.ts +9 -0
  22. package/dist/cli/sarif.d.ts.map +1 -0
  23. package/dist/cli/sarif.js +104 -0
  24. package/dist/lib/analyze.d.ts +10 -0
  25. package/dist/lib/analyze.d.ts.map +1 -0
  26. package/dist/lib/analyze.js +83 -0
  27. package/dist/lib/classifier.d.ts +8 -0
  28. package/dist/lib/classifier.d.ts.map +1 -0
  29. package/dist/lib/classifier.js +74 -0
  30. package/dist/lib/diff.d.ts +15 -0
  31. package/dist/lib/diff.d.ts.map +1 -0
  32. package/dist/lib/diff.js +130 -0
  33. package/dist/lib/errors.d.ts +56 -0
  34. package/dist/lib/errors.d.ts.map +1 -0
  35. package/dist/lib/errors.js +65 -0
  36. package/dist/lib/evaluate.d.ts +19 -0
  37. package/dist/lib/evaluate.d.ts.map +1 -0
  38. package/dist/lib/evaluate.js +189 -0
  39. package/dist/lib/health-score.d.ts +18 -0
  40. package/dist/lib/health-score.d.ts.map +1 -0
  41. package/dist/lib/health-score.js +74 -0
  42. package/dist/lib/html-report.d.ts +15 -0
  43. package/dist/lib/html-report.d.ts.map +1 -0
  44. package/dist/lib/html-report.js +299 -0
  45. package/dist/lib/index.d.ts +26 -0
  46. package/dist/lib/index.d.ts.map +1 -0
  47. package/dist/lib/index.js +24 -0
  48. package/dist/lib/normalizer.d.ts +18 -0
  49. package/dist/lib/normalizer.d.ts.map +1 -0
  50. package/dist/lib/normalizer.js +201 -0
  51. package/dist/lib/rule-engine.d.ts +12 -0
  52. package/dist/lib/rule-engine.d.ts.map +1 -0
  53. package/dist/lib/rule-engine.js +122 -0
  54. package/dist/lib/sanitizer.d.ts +10 -0
  55. package/dist/lib/sanitizer.d.ts.map +1 -0
  56. package/dist/lib/sanitizer.js +129 -0
  57. package/dist/lib/schema.d.ts +85 -0
  58. package/dist/lib/schema.d.ts.map +1 -0
  59. package/dist/lib/schema.js +1 -0
  60. package/dist/lib/trace-sanitizer.d.ts +30 -0
  61. package/dist/lib/trace-sanitizer.d.ts.map +1 -0
  62. package/dist/lib/trace-sanitizer.js +85 -0
  63. package/dist/lib/types.d.ts +161 -0
  64. package/dist/lib/types.d.ts.map +1 -0
  65. package/dist/lib/types.js +1 -0
  66. package/dist/lib/unbatched-detect.d.ts +7 -0
  67. package/dist/lib/unbatched-detect.d.ts.map +1 -0
  68. package/dist/lib/unbatched-detect.js +59 -0
  69. package/dist/lib/validator.d.ts +4 -0
  70. package/dist/lib/validator.d.ts.map +1 -0
  71. package/dist/lib/validator.js +409 -0
  72. package/package.json +98 -0
  73. package/rules/generic/issue-rules.yaml +292 -0
  74. package/rules/generic/shared/base-conditions.yaml +28 -0
  75. package/rules/generic/shared/filters.yaml +12 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalizer.d.ts","sourceRoot":"","sources":["../../src/lib/normalizer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,GAAG,EAAS,MAAM,YAAY,CAAA;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAmC,MAAM,YAAY,CAAA;AAElF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAiIjD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,eAAe,EAAE,CAAA;IAC1B,QAAQ,EAAE,eAAe,EAAE,CAAA;CAC5B;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,GAAG,eAAe,CA4DtD;AAID,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAyB3C"}
@@ -0,0 +1,201 @@
1
+ import { HarError, HAR_ERRORS, createWarning } from './errors.js';
2
+ // ── Resource type detection ─────────────────────────────────────
3
+ const MIME_TO_RESOURCE = [
4
+ [/^text\/html/i, 'document'],
5
+ [/javascript/i, 'script'],
6
+ [/^text\/css/i, 'stylesheet'],
7
+ [/^image\//i, 'image'],
8
+ [/^font\/|\/woff|\/woff2|\/ttf|\/otf/i, 'font'],
9
+ [/^audio\/|^video\//i, 'media'],
10
+ [/^text\/event-stream/i, 'fetch'],
11
+ ];
12
+ function detectResourceType(entry) {
13
+ const url = entry.request?.url ?? '';
14
+ const mimeType = entry.response?.content?.mimeType ?? '';
15
+ // WebSocket detection
16
+ if (url.startsWith('wss://') || url.startsWith('ws://'))
17
+ return 'websocket';
18
+ if (entry.response?.status === 101)
19
+ return 'websocket';
20
+ // XHR/Fetch via _resourceType hint (Chrome DevTools export)
21
+ const resourceHint = entry._resourceType;
22
+ if (resourceHint) {
23
+ const lower = resourceHint.toLowerCase();
24
+ if (lower === 'xhr')
25
+ return 'xhr';
26
+ if (lower === 'fetch')
27
+ return 'fetch';
28
+ if (lower === 'websocket')
29
+ return 'websocket';
30
+ if (lower === 'document')
31
+ return 'document';
32
+ if (lower === 'script')
33
+ return 'script';
34
+ if (lower === 'stylesheet')
35
+ return 'stylesheet';
36
+ if (lower === 'image')
37
+ return 'image';
38
+ if (lower === 'font')
39
+ return 'font';
40
+ if (lower === 'media')
41
+ return 'media';
42
+ }
43
+ // MIME-based detection
44
+ for (const [pattern, type] of MIME_TO_RESOURCE) {
45
+ if (pattern.test(mimeType))
46
+ return type;
47
+ }
48
+ // URL extension fallback
49
+ const pathname = safeUrlPathname(url);
50
+ if (/\.(js|mjs|cjs)$/i.test(pathname))
51
+ return 'script';
52
+ if (/\.css$/i.test(pathname))
53
+ return 'stylesheet';
54
+ if (/\.(png|jpg|jpeg|gif|svg|webp|ico|avif)$/i.test(pathname))
55
+ return 'image';
56
+ if (/\.(woff2?|ttf|otf|eot)$/i.test(pathname))
57
+ return 'font';
58
+ if (/\.(mp[34]|webm|ogg|wav|flac)$/i.test(pathname))
59
+ return 'media';
60
+ // Default for API-like calls
61
+ if (mimeType.includes('json') || mimeType.includes('xml'))
62
+ return 'xhr';
63
+ return 'other';
64
+ }
65
+ function safeUrlPathname(url) {
66
+ try {
67
+ return new URL(url).pathname;
68
+ }
69
+ catch {
70
+ // Handle malformed URLs by extracting path manually
71
+ const pathStart = url.indexOf('/', url.indexOf('//') + 2);
72
+ const queryStart = url.indexOf('?');
73
+ if (pathStart === -1)
74
+ return '';
75
+ return queryStart === -1 ? url.slice(pathStart) : url.slice(pathStart, queryStart);
76
+ }
77
+ }
78
+ // ── Timing normalization ────────────────────────────────────────
79
+ function clampTiming(value) {
80
+ if (value === undefined || value < 0)
81
+ return 0;
82
+ return value;
83
+ }
84
+ function normalizeTimings(entry) {
85
+ const t = entry.timings;
86
+ if (!t) {
87
+ return { blocked: 0, dns: 0, connect: 0, ssl: 0, send: 0, wait: 0, receive: 0, total: 0 };
88
+ }
89
+ const blocked = clampTiming(t.blocked);
90
+ const dns = clampTiming(t.dns);
91
+ const connect = clampTiming(t.connect);
92
+ const ssl = clampTiming(t.ssl);
93
+ const send = clampTiming(t.send);
94
+ const wait = clampTiming(t.wait);
95
+ const receive = clampTiming(t.receive);
96
+ const total = blocked + dns + connect + ssl + send + wait + receive;
97
+ return { blocked, dns, connect, ssl, send, wait, receive, total };
98
+ }
99
+ // ── Long-poll detection ─────────────────────────────────────────
100
+ function detectLongPoll(entry, timings) {
101
+ // SSE (Server-Sent Events)
102
+ const mimeType = entry.response?.content?.mimeType ?? '';
103
+ if (mimeType === 'text/event-stream')
104
+ return true;
105
+ // High wait time with small response body
106
+ if (timings.wait > 25000) {
107
+ const bodySize = entry.response?.content?.size ?? 0;
108
+ if (bodySize < 1024)
109
+ return true;
110
+ }
111
+ return false;
112
+ }
113
+ // ── Transfer size resolution ────────────────────────────────────
114
+ function resolveTransferSize(entry) {
115
+ // Chrome DevTools _transferSize extension field
116
+ const chromeTransfer = entry._transferSize;
117
+ if (typeof chromeTransfer === 'number' && chromeTransfer > 0)
118
+ return chromeTransfer;
119
+ // Standard HAR response.bodySize
120
+ const bodySize = entry.response?.bodySize;
121
+ if (typeof bodySize === 'number' && bodySize > 0)
122
+ return bodySize;
123
+ // Fallback to content.size
124
+ const contentSize = entry.response?.content?.size;
125
+ if (typeof contentSize === 'number' && contentSize > 0)
126
+ return contentSize;
127
+ return 0;
128
+ }
129
+ export function normalizeHar(har) {
130
+ const warnings = [];
131
+ if (!har?.log?.entries || !Array.isArray(har.log.entries)) {
132
+ throw new HarError({
133
+ code: HAR_ERRORS.HAR002,
134
+ message: 'Invalid HAR: missing log.entries array',
135
+ help: 'Ensure the HAR file has a valid { log: { entries: [...] } } structure.',
136
+ });
137
+ }
138
+ if (har.log.entries.length === 0) {
139
+ throw new HarError({
140
+ code: HAR_ERRORS.HAR003,
141
+ message: 'HAR file contains no entries',
142
+ help: 'The HAR file has an empty entries array. Capture some network traffic and re-export.',
143
+ });
144
+ }
145
+ // Parse all start times, find the earliest
146
+ const startTimes = [];
147
+ for (const entry of har.log.entries) {
148
+ const ms = new Date(entry.startedDateTime).getTime();
149
+ if (isNaN(ms)) {
150
+ warnings.push(createWarning(HAR_ERRORS.HAR002, `Invalid startedDateTime: ${entry.startedDateTime}`, 'This entry has an unparseable timestamp and will use time 0.'));
151
+ startTimes.push(0);
152
+ }
153
+ else {
154
+ startTimes.push(ms);
155
+ }
156
+ }
157
+ const baseTime = Math.min(...startTimes.filter((t) => t > 0)) || 0;
158
+ const entries = har.log.entries.map((entry, i) => {
159
+ const timings = normalizeTimings(entry);
160
+ const resourceType = detectResourceType(entry);
161
+ const isWebSocket = resourceType === 'websocket';
162
+ const isLongPoll = !isWebSocket && detectLongPoll(entry, timings);
163
+ return {
164
+ entry,
165
+ startTimeMs: startTimes[i] > 0 ? startTimes[i] - baseTime : 0,
166
+ totalDuration: timings.total,
167
+ transferSizeResolved: resolveTransferSize(entry),
168
+ contentSize: Math.max(0, entry.response?.content?.size ?? 0),
169
+ timings,
170
+ resourceType,
171
+ isLongPoll,
172
+ isWebSocket,
173
+ httpVersion: entry.request?.httpVersion ?? 'unknown',
174
+ };
175
+ });
176
+ return { entries, warnings };
177
+ }
178
+ // ── HAR parsing (JSON string -> Har object) ─────────────────────
179
+ export function parseHar(input) {
180
+ let parsed;
181
+ try {
182
+ parsed = JSON.parse(input);
183
+ }
184
+ catch (e) {
185
+ throw new HarError({
186
+ code: HAR_ERRORS.HAR001,
187
+ message: `Invalid JSON: ${e instanceof Error ? e.message : 'parse error'}`,
188
+ help: 'The input is not valid JSON. Ensure the file is a properly formatted HAR export.',
189
+ });
190
+ }
191
+ if (typeof parsed !== 'object' ||
192
+ parsed === null ||
193
+ !('log' in parsed)) {
194
+ throw new HarError({
195
+ code: HAR_ERRORS.HAR002,
196
+ message: 'Not a HAR file: missing top-level "log" property',
197
+ help: 'A valid HAR file must have a { "log": { ... } } structure. Check that you exported from the Network tab correctly.',
198
+ });
199
+ }
200
+ return parsed;
201
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Rule engine: evaluates YAML-defined rules against NormalizedEntry arrays.
3
+ *
4
+ * Adapted from the reference engine (yaml_base RuleEngine.ts).
5
+ * Generalized: DisplayEntry -> NormalizedEntry, stripped SF-specific imports.
6
+ */
7
+ import type { NormalizedEntry, Finding } from './types.js';
8
+ import type { IssueRulesFile, YamlRule, ConditionGroup, SharedConditionsFile, FiltersFile } from './schema.js';
9
+ export declare function resolveComposition(rule: YamlRule, sharedConditions?: SharedConditionsFile): ConditionGroup | undefined;
10
+ export declare function evaluateRules(rulesFile: IssueRulesFile, entries: NormalizedEntry[], sharedConditions?: SharedConditionsFile, filtersFile?: FiltersFile): Finding[];
11
+ export declare function getRootCauseWeights(rulesFile: IssueRulesFile): Map<string, Record<string, number>>;
12
+ //# sourceMappingURL=rule-engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule-engine.d.ts","sourceRoot":"","sources":["../../src/lib/rule-engine.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAgC,MAAM,YAAY,CAAA;AACxF,OAAO,KAAK,EACV,cAAc,EACd,QAAQ,EACR,cAAc,EAEd,oBAAoB,EACpB,WAAW,EACZ,MAAM,aAAa,CAAA;AAWpB,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,QAAQ,EACd,gBAAgB,CAAC,EAAE,oBAAoB,GACtC,cAAc,GAAG,SAAS,CAgB5B;AAuHD,wBAAgB,aAAa,CAC3B,SAAS,EAAE,cAAc,EACzB,OAAO,EAAE,eAAe,EAAE,EAC1B,gBAAgB,CAAC,EAAE,oBAAoB,EACvC,WAAW,CAAC,EAAE,WAAW,GACxB,OAAO,EAAE,CAOX;AAID,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,cAAc,GACxB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAYrC"}
@@ -0,0 +1,122 @@
1
+ import { evaluateCondition, evaluateFieldCondition, computeSeverity, computeImpact, interpolate, } from './evaluate.js';
2
+ // ── Composition resolution ──────────────────────────────────────
3
+ export function resolveComposition(rule, sharedConditions) {
4
+ if (!rule.inherits || !sharedConditions)
5
+ return rule.condition;
6
+ const inherited = [];
7
+ for (const name of rule.inherits) {
8
+ const cond = sharedConditions.conditions[name];
9
+ if (!cond)
10
+ continue;
11
+ if (rule.overrides?.[name]) {
12
+ inherited.push(rule.overrides[name]);
13
+ }
14
+ else {
15
+ inherited.push(cond);
16
+ }
17
+ }
18
+ const existing = rule.condition?.match_all ?? [];
19
+ return { match_all: [...inherited, ...existing] };
20
+ }
21
+ function matchesExcludeFilter(entry, excludeNames, filtersFile) {
22
+ if (!excludeNames || !filtersFile)
23
+ return false;
24
+ for (const name of excludeNames) {
25
+ const filter = filtersFile.filters[name];
26
+ if (filter && evaluateFieldCondition(entry, filter))
27
+ return true;
28
+ }
29
+ return false;
30
+ }
31
+ // ── Single rule evaluation ──────────────────────────────────────
32
+ function evaluateRule(ruleId, rule, entries, sharedConditions, filtersFile) {
33
+ if (rule.type === 'aggregate') {
34
+ return evaluateAggregateRule(ruleId, rule, entries);
35
+ }
36
+ if (rule.prerequisite) {
37
+ const prereqMet = entries.some((e) => evaluateFieldCondition(e, rule.prerequisite.any_entry_matches));
38
+ if (!prereqMet)
39
+ return null;
40
+ }
41
+ const resolvedCondition = resolveComposition(rule, sharedConditions) ?? rule.condition;
42
+ const affectedIndices = [];
43
+ if (resolvedCondition) {
44
+ for (let i = 0; i < entries.length; i++) {
45
+ if (matchesExcludeFilter(entries[i], rule.exclude, filtersFile))
46
+ continue;
47
+ if (evaluateCondition(entries[i], resolvedCondition)) {
48
+ affectedIndices.push(i);
49
+ }
50
+ }
51
+ }
52
+ const minCount = rule.min_count ?? 1;
53
+ if (affectedIndices.length < minCount)
54
+ return null;
55
+ const severity = computeSeverity(rule.severity, rule.severity_escalation, affectedIndices.length, entries.length);
56
+ const impact = computeImpact(entries, affectedIndices, rule.impact);
57
+ const count = affectedIndices.length;
58
+ const vars = {
59
+ count,
60
+ total: entries.length,
61
+ impact: Math.round(impact),
62
+ s: count !== 1 ? 's' : '',
63
+ };
64
+ return {
65
+ ruleId,
66
+ category: rule.category,
67
+ severity,
68
+ title: interpolate(rule.title, vars),
69
+ description: interpolate(rule.description, vars),
70
+ recommendation: interpolate(rule.recommendation, vars),
71
+ affectedEntries: affectedIndices,
72
+ impact,
73
+ };
74
+ }
75
+ function evaluateAggregateRule(ruleId, rule, entries) {
76
+ if (rule.aggregate_condition?.min_entries !== undefined) {
77
+ if (entries.length < rule.aggregate_condition.min_entries)
78
+ return null;
79
+ }
80
+ const severity = computeSeverity(rule.severity, rule.severity_escalation, entries.length, entries.length);
81
+ const vars = {
82
+ count: entries.length,
83
+ total: entries.length,
84
+ impact: 0,
85
+ s: entries.length !== 1 ? 's' : '',
86
+ };
87
+ return {
88
+ ruleId,
89
+ category: rule.category,
90
+ severity,
91
+ title: interpolate(rule.title, vars),
92
+ description: interpolate(rule.description, vars),
93
+ recommendation: interpolate(rule.recommendation, vars),
94
+ affectedEntries: [],
95
+ impact: 0,
96
+ };
97
+ }
98
+ // ── Main evaluation ─────────────────────────────────────────────
99
+ export function evaluateRules(rulesFile, entries, sharedConditions, filtersFile) {
100
+ const findings = [];
101
+ for (const [ruleId, rule] of Object.entries(rulesFile.rules)) {
102
+ const finding = evaluateRule(ruleId, rule, entries, sharedConditions, filtersFile);
103
+ if (finding)
104
+ findings.push(finding);
105
+ }
106
+ return findings;
107
+ }
108
+ // ── Root cause weight extraction ────────────────────────────────
109
+ export function getRootCauseWeights(rulesFile) {
110
+ const weights = new Map();
111
+ for (const [ruleId, rule] of Object.entries(rulesFile.rules)) {
112
+ if (rule.root_cause_weight) {
113
+ const w = {};
114
+ for (const [k, v] of Object.entries(rule.root_cause_weight)) {
115
+ if (v !== undefined)
116
+ w[k] = v;
117
+ }
118
+ weights.set(ruleId, w);
119
+ }
120
+ }
121
+ return weights;
122
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * HAR sanitizer: aggressive + selective modes.
3
+ *
4
+ * Operates on raw HAR objects. Produces a deep-cloned sanitized copy.
5
+ * Never mutates the input. Library returns raw data, sanitize() is explicit.
6
+ */
7
+ import type { Har } from 'har-format';
8
+ import type { SanitizeOptions } from './types.js';
9
+ export declare function sanitize(har: Har, options?: SanitizeOptions): Har;
10
+ //# sourceMappingURL=sanitizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitizer.d.ts","sourceRoot":"","sources":["../../src/lib/sanitizer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,GAAG,EAAiB,MAAM,YAAY,CAAA;AACpD,OAAO,KAAK,EAAE,eAAe,EAAoB,MAAM,YAAY,CAAA;AA6JnE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,GAAG,CAejE"}
@@ -0,0 +1,129 @@
1
+ // ── Sensitive patterns ──────────────────────────────────────────
2
+ const SENSITIVE_HEADERS = new Set([
3
+ 'authorization',
4
+ 'cookie',
5
+ 'set-cookie',
6
+ 'x-csrf-token',
7
+ 'x-xsrf-token',
8
+ 'proxy-authorization',
9
+ ]);
10
+ const SENSITIVE_QUERY_NAMES = new Set([
11
+ 'token',
12
+ 'key',
13
+ 'secret',
14
+ 'password',
15
+ 'pwd',
16
+ 'jwt',
17
+ 'session',
18
+ 'auth',
19
+ 'api_key',
20
+ 'apikey',
21
+ 'access_token',
22
+ 'refresh_token',
23
+ 'client_secret',
24
+ ]);
25
+ // JWT pattern: three base64url segments separated by dots
26
+ const JWT_PATTERN = /^eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
27
+ // High-entropy string heuristic: 20+ chars of base64-like content
28
+ const HIGH_ENTROPY_PATTERN = /^[A-Za-z0-9+/=_-]{20,}$/;
29
+ const REDACTED = '[REDACTED]';
30
+ // ── Sanitization functions ──────────────────────────────────────
31
+ function sanitizeHeaders(headers, categories) {
32
+ return headers.map((h) => {
33
+ const name = h.name.toLowerCase();
34
+ if (categories.has('auth-headers') && SENSITIVE_HEADERS.has(name)) {
35
+ return { name: h.name, value: REDACTED };
36
+ }
37
+ if (categories.has('cookies') && (name === 'cookie' || name === 'set-cookie')) {
38
+ return { name: h.name, value: REDACTED };
39
+ }
40
+ if (categories.has('response-cookies') && name === 'set-cookie') {
41
+ return { name: h.name, value: REDACTED };
42
+ }
43
+ if (categories.has('jwt-signatures') && JWT_PATTERN.test(h.value)) {
44
+ // Preserve header and payload, redact signature
45
+ const parts = h.value.split('.');
46
+ return { name: h.name, value: `${parts[0]}.${parts[1]}.[SIGNATURE_REDACTED]` };
47
+ }
48
+ if (categories.has('high-entropy') && HIGH_ENTROPY_PATTERN.test(h.value) && h.value.length > 40) {
49
+ return { name: h.name, value: REDACTED };
50
+ }
51
+ return { ...h };
52
+ });
53
+ }
54
+ function sanitizeUrl(url, categories) {
55
+ if (!categories.has('query-params'))
56
+ return url;
57
+ try {
58
+ const parsed = new URL(url);
59
+ const params = new URLSearchParams(parsed.search);
60
+ let changed = false;
61
+ for (const [key] of params) {
62
+ if (SENSITIVE_QUERY_NAMES.has(key.toLowerCase())) {
63
+ params.set(key, REDACTED);
64
+ changed = true;
65
+ }
66
+ }
67
+ if (changed) {
68
+ parsed.search = params.toString();
69
+ return parsed.toString();
70
+ }
71
+ return url;
72
+ }
73
+ catch {
74
+ return url;
75
+ }
76
+ }
77
+ function sanitizeCookies(cookies) {
78
+ if (!cookies)
79
+ return undefined;
80
+ return cookies.map((c) => ({ ...c, value: REDACTED }));
81
+ }
82
+ function sanitizeEntry(entry, categories) {
83
+ const sanitized = JSON.parse(JSON.stringify(entry));
84
+ // URL sanitization
85
+ if (sanitized.request) {
86
+ sanitized.request.url = sanitizeUrl(sanitized.request.url, categories);
87
+ if (sanitized.request.headers) {
88
+ sanitized.request.headers = sanitizeHeaders(sanitized.request.headers, categories);
89
+ }
90
+ if (categories.has('query-params') && sanitized.request.queryString) {
91
+ sanitized.request.queryString = sanitized.request.queryString.map((q) => {
92
+ if (SENSITIVE_QUERY_NAMES.has(q.name.toLowerCase())) {
93
+ return { ...q, value: REDACTED };
94
+ }
95
+ return q;
96
+ });
97
+ }
98
+ if (categories.has('cookies') && sanitized.request.cookies) {
99
+ sanitized.request.cookies = sanitizeCookies(sanitized.request.cookies);
100
+ }
101
+ }
102
+ if (sanitized.response) {
103
+ if (sanitized.response.headers) {
104
+ sanitized.response.headers = sanitizeHeaders(sanitized.response.headers, categories);
105
+ }
106
+ if (categories.has('response-cookies') && sanitized.response.cookies) {
107
+ sanitized.response.cookies = sanitizeCookies(sanitized.response.cookies);
108
+ }
109
+ }
110
+ return sanitized;
111
+ }
112
+ // ── Main sanitize ───────────────────────────────────────────────
113
+ const ALL_CATEGORIES = [
114
+ 'cookies',
115
+ 'auth-headers',
116
+ 'query-params',
117
+ 'response-cookies',
118
+ 'jwt-signatures',
119
+ 'high-entropy',
120
+ ];
121
+ export function sanitize(har, options) {
122
+ const mode = options?.mode ?? 'aggressive';
123
+ const categories = new Set(mode === 'aggressive' ? ALL_CATEGORIES : (options?.categories ?? ALL_CATEGORIES));
124
+ const sanitized = JSON.parse(JSON.stringify(har));
125
+ if (sanitized.log?.entries) {
126
+ sanitized.log.entries = sanitized.log.entries.map((entry) => sanitizeEntry(entry, categories));
127
+ }
128
+ return sanitized;
129
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * TypeScript types for the YAML rule schema.
3
+ * Defines the contract for rule files, shared conditions, and filters.
4
+ */
5
+ import type { IssueSeverity, IssueCategory } from './types.js';
6
+ export interface FieldCondition {
7
+ field: string;
8
+ field_fallback?: string;
9
+ equals?: unknown;
10
+ not_equals?: unknown;
11
+ in?: unknown[];
12
+ not_in?: unknown[];
13
+ gt?: number;
14
+ gte?: number;
15
+ lt?: number;
16
+ lte?: number;
17
+ matches?: string;
18
+ not_matches?: string;
19
+ }
20
+ export interface ResponseHeaderSpec {
21
+ name: string;
22
+ value_matches?: string;
23
+ value_gt?: number;
24
+ value_lt?: number;
25
+ }
26
+ export interface ResponseHeaderCondition {
27
+ has_response_header?: ResponseHeaderSpec;
28
+ no_response_header?: Omit<ResponseHeaderSpec, 'value_gt' | 'value_lt'>;
29
+ }
30
+ export interface ConditionGroup {
31
+ match_all?: ConditionNode[];
32
+ match_any?: ConditionNode[];
33
+ }
34
+ export type ConditionNode = FieldCondition | ResponseHeaderCondition | ConditionGroup;
35
+ export interface SeverityEscalation {
36
+ warning_threshold?: number;
37
+ critical_threshold?: number;
38
+ warning_ratio?: number;
39
+ critical_ratio?: number;
40
+ }
41
+ export interface ImpactSpec {
42
+ field?: string;
43
+ fields?: string[];
44
+ baseline?: number;
45
+ value?: number;
46
+ }
47
+ export interface RootCauseWeight {
48
+ server?: number;
49
+ network?: number;
50
+ client?: number;
51
+ }
52
+ export interface PrerequisiteSpec {
53
+ any_entry_matches: FieldCondition;
54
+ }
55
+ export interface AggregateCondition {
56
+ min_entries?: number;
57
+ }
58
+ export interface YamlRule {
59
+ category: IssueCategory | string;
60
+ severity: IssueSeverity;
61
+ severity_escalation?: SeverityEscalation;
62
+ title: string;
63
+ description: string;
64
+ recommendation: string;
65
+ condition?: ConditionGroup;
66
+ min_count?: number;
67
+ type?: 'per_entry' | 'aggregate';
68
+ aggregate_condition?: AggregateCondition;
69
+ prerequisite?: PrerequisiteSpec;
70
+ impact?: ImpactSpec;
71
+ root_cause_weight?: RootCauseWeight;
72
+ inherits?: string[];
73
+ exclude?: string[];
74
+ overrides?: Record<string, FieldCondition>;
75
+ }
76
+ export interface IssueRulesFile {
77
+ rules: Record<string, YamlRule>;
78
+ }
79
+ export interface SharedConditionsFile {
80
+ conditions: Record<string, FieldCondition>;
81
+ }
82
+ export interface FiltersFile {
83
+ filters: Record<string, FieldCondition>;
84
+ }
85
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/lib/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Trace sanitizer: sanitizes output for verbose/trace/error messages.
3
+ * Public exports used by CLI --verbose, browser RuleTestRunner, and error formatting.
4
+ *
5
+ * Three functions:
6
+ * sanitizeTrace(text) - general-purpose, catches headers + URLs + tokens
7
+ * sanitizeUrl(url) - strips query params, preserves path
8
+ * sanitizeHeaderValue(name, value) - redacts value for sensitive headers
9
+ */
10
+ /**
11
+ * Strip query parameters from a URL. Preserves path and fragment.
12
+ * Handles malformed URLs gracefully (returns input unchanged if unparseable).
13
+ */
14
+ export declare function sanitizeUrl(url: string): string;
15
+ /**
16
+ * Redact the value of sensitive headers. Non-sensitive headers pass through.
17
+ * Case-insensitive header name matching.
18
+ */
19
+ export declare function sanitizeHeaderValue(name: string, value: string): string;
20
+ /**
21
+ * General-purpose trace sanitizer. Catches:
22
+ * - Header values (Authorization: Bearer xxx -> Authorization: [REDACTED])
23
+ * - URL query params (https://example.com?token=abc -> https://example.com)
24
+ * - Bearer tokens
25
+ * - JWT-like strings
26
+ *
27
+ * Handles multiline input. Returns empty string for null/undefined input.
28
+ */
29
+ export declare function sanitizeTrace(text: string): string;
30
+ //# sourceMappingURL=trace-sanitizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trace-sanitizer.d.ts","sourceRoot":"","sources":["../../src/lib/trace-sanitizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAiBH;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAgB/C;AAID;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKvE;AAUD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkBlD"}