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.
- package/LICENSE +661 -0
- package/README.md +179 -0
- package/completions/har-o-scope.bash +64 -0
- package/completions/har-o-scope.fish +43 -0
- package/completions/har-o-scope.zsh +63 -0
- package/dist/cli/colors.d.ts +17 -0
- package/dist/cli/colors.d.ts.map +1 -0
- package/dist/cli/colors.js +54 -0
- package/dist/cli/demo.d.ts +7 -0
- package/dist/cli/demo.d.ts.map +1 -0
- package/dist/cli/demo.js +62 -0
- package/dist/cli/formatters.d.ts +12 -0
- package/dist/cli/formatters.d.ts.map +1 -0
- package/dist/cli/formatters.js +249 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +260 -0
- package/dist/cli/rules.d.ts +3 -0
- package/dist/cli/rules.d.ts.map +1 -0
- package/dist/cli/rules.js +36 -0
- package/dist/cli/sarif.d.ts +9 -0
- package/dist/cli/sarif.d.ts.map +1 -0
- package/dist/cli/sarif.js +104 -0
- package/dist/lib/analyze.d.ts +10 -0
- package/dist/lib/analyze.d.ts.map +1 -0
- package/dist/lib/analyze.js +83 -0
- package/dist/lib/classifier.d.ts +8 -0
- package/dist/lib/classifier.d.ts.map +1 -0
- package/dist/lib/classifier.js +74 -0
- package/dist/lib/diff.d.ts +15 -0
- package/dist/lib/diff.d.ts.map +1 -0
- package/dist/lib/diff.js +130 -0
- package/dist/lib/errors.d.ts +56 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +65 -0
- package/dist/lib/evaluate.d.ts +19 -0
- package/dist/lib/evaluate.d.ts.map +1 -0
- package/dist/lib/evaluate.js +189 -0
- package/dist/lib/health-score.d.ts +18 -0
- package/dist/lib/health-score.d.ts.map +1 -0
- package/dist/lib/health-score.js +74 -0
- package/dist/lib/html-report.d.ts +15 -0
- package/dist/lib/html-report.d.ts.map +1 -0
- package/dist/lib/html-report.js +299 -0
- package/dist/lib/index.d.ts +26 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +24 -0
- package/dist/lib/normalizer.d.ts +18 -0
- package/dist/lib/normalizer.d.ts.map +1 -0
- package/dist/lib/normalizer.js +201 -0
- package/dist/lib/rule-engine.d.ts +12 -0
- package/dist/lib/rule-engine.d.ts.map +1 -0
- package/dist/lib/rule-engine.js +122 -0
- package/dist/lib/sanitizer.d.ts +10 -0
- package/dist/lib/sanitizer.d.ts.map +1 -0
- package/dist/lib/sanitizer.js +129 -0
- package/dist/lib/schema.d.ts +85 -0
- package/dist/lib/schema.d.ts.map +1 -0
- package/dist/lib/schema.js +1 -0
- package/dist/lib/trace-sanitizer.d.ts +30 -0
- package/dist/lib/trace-sanitizer.d.ts.map +1 -0
- package/dist/lib/trace-sanitizer.js +85 -0
- package/dist/lib/types.d.ts +161 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/unbatched-detect.d.ts +7 -0
- package/dist/lib/unbatched-detect.d.ts.map +1 -0
- package/dist/lib/unbatched-detect.js +59 -0
- package/dist/lib/validator.d.ts +4 -0
- package/dist/lib/validator.d.ts.map +1 -0
- package/dist/lib/validator.js +409 -0
- package/package.json +98 -0
- package/rules/generic/issue-rules.yaml +292 -0
- package/rules/generic/shared/base-conditions.yaml +28 -0
- package/rules/generic/shared/filters.yaml +12 -0
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
const SENSITIVE_HEADER_NAMES = new Set([
|
|
11
|
+
'authorization',
|
|
12
|
+
'cookie',
|
|
13
|
+
'set-cookie',
|
|
14
|
+
'x-csrf-token',
|
|
15
|
+
'x-xsrf-token',
|
|
16
|
+
'proxy-authorization',
|
|
17
|
+
'x-api-key',
|
|
18
|
+
'x-auth-token',
|
|
19
|
+
]);
|
|
20
|
+
const REDACTED = '[REDACTED]';
|
|
21
|
+
// ── sanitizeUrl ─────────────────────────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* Strip query parameters from a URL. Preserves path and fragment.
|
|
24
|
+
* Handles malformed URLs gracefully (returns input unchanged if unparseable).
|
|
25
|
+
*/
|
|
26
|
+
export function sanitizeUrl(url) {
|
|
27
|
+
if (!url)
|
|
28
|
+
return url;
|
|
29
|
+
try {
|
|
30
|
+
const parsed = new URL(url);
|
|
31
|
+
// Clear all query params
|
|
32
|
+
parsed.search = '';
|
|
33
|
+
return parsed.toString();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Fallback: strip everything after '?'
|
|
37
|
+
const queryIdx = url.indexOf('?');
|
|
38
|
+
if (queryIdx === -1)
|
|
39
|
+
return url;
|
|
40
|
+
const fragIdx = url.indexOf('#');
|
|
41
|
+
if (fragIdx !== -1 && fragIdx < queryIdx)
|
|
42
|
+
return url;
|
|
43
|
+
return fragIdx > queryIdx ? url.slice(0, queryIdx) + url.slice(fragIdx) : url.slice(0, queryIdx);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// ── sanitizeHeaderValue ─────────────────────────────────────────
|
|
47
|
+
/**
|
|
48
|
+
* Redact the value of sensitive headers. Non-sensitive headers pass through.
|
|
49
|
+
* Case-insensitive header name matching.
|
|
50
|
+
*/
|
|
51
|
+
export function sanitizeHeaderValue(name, value) {
|
|
52
|
+
if (SENSITIVE_HEADER_NAMES.has(name.toLowerCase())) {
|
|
53
|
+
return REDACTED;
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
// ── sanitizeTrace ───────────────────────────────────────────────
|
|
58
|
+
// Patterns for common sensitive values in trace text
|
|
59
|
+
const HEADER_VALUE_PATTERN = /^(\s*(?:Authorization|Cookie|Set-Cookie|X-CSRF-Token|X-XSRF-Token|Proxy-Authorization|X-Api-Key|X-Auth-Token)\s*:\s*)(.+)$/gim;
|
|
60
|
+
const URL_QUERY_PATTERN = /https?:\/\/[^\s"']+\?[^\s"']+/gi;
|
|
61
|
+
const BEARER_PATTERN = /Bearer\s+[A-Za-z0-9._~+/=-]+/gi;
|
|
62
|
+
const JWT_LIKE_PATTERN = /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
|
|
63
|
+
/**
|
|
64
|
+
* General-purpose trace sanitizer. Catches:
|
|
65
|
+
* - Header values (Authorization: Bearer xxx -> Authorization: [REDACTED])
|
|
66
|
+
* - URL query params (https://example.com?token=abc -> https://example.com)
|
|
67
|
+
* - Bearer tokens
|
|
68
|
+
* - JWT-like strings
|
|
69
|
+
*
|
|
70
|
+
* Handles multiline input. Returns empty string for null/undefined input.
|
|
71
|
+
*/
|
|
72
|
+
export function sanitizeTrace(text) {
|
|
73
|
+
if (!text)
|
|
74
|
+
return '';
|
|
75
|
+
let result = text;
|
|
76
|
+
// Redact header values (must be first to catch Authorization: Bearer xxx)
|
|
77
|
+
result = result.replace(HEADER_VALUE_PATTERN, `$1${REDACTED}`);
|
|
78
|
+
// Redact Bearer tokens not already caught by header patterns
|
|
79
|
+
result = result.replace(BEARER_PATTERN, `Bearer ${REDACTED}`);
|
|
80
|
+
// Redact JWTs
|
|
81
|
+
result = result.replace(JWT_LIKE_PATTERN, `${REDACTED}`);
|
|
82
|
+
// Strip query params from URLs
|
|
83
|
+
result = result.replace(URL_QUERY_PATTERN, (match) => sanitizeUrl(match));
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core type definitions for har-o-scope.
|
|
3
|
+
* No DOM dependencies. Used by library, CLI, and Web Worker.
|
|
4
|
+
*/
|
|
5
|
+
import type { Har, Entry, Header } from 'har-format';
|
|
6
|
+
export type ResourceType = 'document' | 'script' | 'stylesheet' | 'image' | 'font' | 'media' | 'xhr' | 'fetch' | 'websocket' | 'other';
|
|
7
|
+
export interface NormalizedTimings {
|
|
8
|
+
blocked: number;
|
|
9
|
+
dns: number;
|
|
10
|
+
connect: number;
|
|
11
|
+
ssl: number;
|
|
12
|
+
send: number;
|
|
13
|
+
wait: number;
|
|
14
|
+
receive: number;
|
|
15
|
+
total: number;
|
|
16
|
+
}
|
|
17
|
+
export interface NormalizedEntry {
|
|
18
|
+
/** Original HAR entry */
|
|
19
|
+
entry: Entry;
|
|
20
|
+
/** Milliseconds since the first entry's startedDateTime */
|
|
21
|
+
startTimeMs: number;
|
|
22
|
+
/** Total request duration in ms (sum of all timing phases) */
|
|
23
|
+
totalDuration: number;
|
|
24
|
+
/** Resolved transfer size: transferSize > 0 ? transferSize : content.size */
|
|
25
|
+
transferSizeResolved: number;
|
|
26
|
+
/** Resolved content size from response.content.size */
|
|
27
|
+
contentSize: number;
|
|
28
|
+
/** Parsed timings with -1 replaced by 0 */
|
|
29
|
+
timings: NormalizedTimings;
|
|
30
|
+
/** Detected resource type */
|
|
31
|
+
resourceType: ResourceType;
|
|
32
|
+
/** True if this looks like a long-poll (wait > 25s + small body, or SSE) */
|
|
33
|
+
isLongPoll: boolean;
|
|
34
|
+
/** True if this is a WebSocket connection */
|
|
35
|
+
isWebSocket: boolean;
|
|
36
|
+
/** HTTP version string from the request */
|
|
37
|
+
httpVersion: string;
|
|
38
|
+
}
|
|
39
|
+
export type IssueSeverity = 'info' | 'warning' | 'critical';
|
|
40
|
+
export type IssueCategory = 'server' | 'network' | 'client' | 'optimization' | 'security' | 'errors' | 'informational' | 'performance';
|
|
41
|
+
export interface Finding {
|
|
42
|
+
/** Rule ID that produced this finding */
|
|
43
|
+
ruleId: string;
|
|
44
|
+
/** Issue category */
|
|
45
|
+
category: IssueCategory;
|
|
46
|
+
/** Final severity (after escalation) */
|
|
47
|
+
severity: IssueSeverity;
|
|
48
|
+
/** Human-readable title (interpolated) */
|
|
49
|
+
title: string;
|
|
50
|
+
/** Detailed description (interpolated) */
|
|
51
|
+
description: string;
|
|
52
|
+
/** Fix recommendation (interpolated) */
|
|
53
|
+
recommendation: string;
|
|
54
|
+
/** Indices into the NormalizedEntry array */
|
|
55
|
+
affectedEntries: number[];
|
|
56
|
+
/** Computed impact value */
|
|
57
|
+
impact: number;
|
|
58
|
+
}
|
|
59
|
+
export interface RootCauseResult {
|
|
60
|
+
client: number;
|
|
61
|
+
network: number;
|
|
62
|
+
server: number;
|
|
63
|
+
}
|
|
64
|
+
export interface AnalysisWarning {
|
|
65
|
+
code: string;
|
|
66
|
+
message: string;
|
|
67
|
+
help: string;
|
|
68
|
+
docsUrl: string;
|
|
69
|
+
}
|
|
70
|
+
export interface AnalysisMetadata {
|
|
71
|
+
rulesEvaluated: number;
|
|
72
|
+
customRulesLoaded: number;
|
|
73
|
+
analysisTimeMs: number;
|
|
74
|
+
totalRequests: number;
|
|
75
|
+
totalTimeMs: number;
|
|
76
|
+
}
|
|
77
|
+
export interface AnalysisResult {
|
|
78
|
+
/** Normalized entries */
|
|
79
|
+
entries: NormalizedEntry[];
|
|
80
|
+
/** Detected findings */
|
|
81
|
+
findings: Finding[];
|
|
82
|
+
/** Root cause classification */
|
|
83
|
+
rootCause: RootCauseResult;
|
|
84
|
+
/** Non-fatal warnings encountered during analysis */
|
|
85
|
+
warnings: AnalysisWarning[];
|
|
86
|
+
/** Analysis metadata */
|
|
87
|
+
metadata: AnalysisMetadata;
|
|
88
|
+
}
|
|
89
|
+
export interface AnalysisOptions {
|
|
90
|
+
/** Path to custom YAML rules file or directory */
|
|
91
|
+
customRules?: string;
|
|
92
|
+
/** Custom rules as parsed YAML objects */
|
|
93
|
+
customRulesData?: unknown[];
|
|
94
|
+
/** Disable built-in rules */
|
|
95
|
+
noBuiltin?: boolean;
|
|
96
|
+
/** Minimum severity to include in findings */
|
|
97
|
+
minSeverity?: IssueSeverity;
|
|
98
|
+
}
|
|
99
|
+
export interface ScoreDeduction {
|
|
100
|
+
reason: string;
|
|
101
|
+
points: number;
|
|
102
|
+
}
|
|
103
|
+
export interface ScoreBreakdown {
|
|
104
|
+
findingDeductions: ScoreDeduction[];
|
|
105
|
+
timingPenalty: number;
|
|
106
|
+
volumePenalty: number;
|
|
107
|
+
confidenceMultiplier: number;
|
|
108
|
+
totalDeductions: number;
|
|
109
|
+
}
|
|
110
|
+
export interface HealthScore {
|
|
111
|
+
score: number;
|
|
112
|
+
breakdown: ScoreBreakdown;
|
|
113
|
+
}
|
|
114
|
+
export interface TimingDelta {
|
|
115
|
+
urlPattern: string;
|
|
116
|
+
beforeCount: number;
|
|
117
|
+
afterCount: number;
|
|
118
|
+
beforeAvgMs: number;
|
|
119
|
+
afterAvgMs: number;
|
|
120
|
+
deltaMs: number;
|
|
121
|
+
deltaPercent: number;
|
|
122
|
+
}
|
|
123
|
+
export interface FindingDelta {
|
|
124
|
+
ruleId: string;
|
|
125
|
+
beforeSeverity: IssueSeverity;
|
|
126
|
+
afterSeverity: IssueSeverity;
|
|
127
|
+
beforeCount: number;
|
|
128
|
+
afterCount: number;
|
|
129
|
+
}
|
|
130
|
+
export interface DiffResult {
|
|
131
|
+
scoreDelta: number;
|
|
132
|
+
newFindings: Finding[];
|
|
133
|
+
resolvedFindings: Finding[];
|
|
134
|
+
persistedFindings: FindingDelta[];
|
|
135
|
+
timingDeltas: TimingDelta[];
|
|
136
|
+
requestCountDelta: number;
|
|
137
|
+
totalTimeDelta: number;
|
|
138
|
+
}
|
|
139
|
+
export type SanitizeMode = 'aggressive' | 'selective';
|
|
140
|
+
export interface SanitizeOptions {
|
|
141
|
+
mode: SanitizeMode;
|
|
142
|
+
/** Categories to sanitize in selective mode */
|
|
143
|
+
categories?: SanitizeCategory[];
|
|
144
|
+
}
|
|
145
|
+
export type SanitizeCategory = 'cookies' | 'auth-headers' | 'query-params' | 'response-cookies' | 'jwt-signatures' | 'high-entropy';
|
|
146
|
+
export interface ValidationError {
|
|
147
|
+
code: string;
|
|
148
|
+
message: string;
|
|
149
|
+
line?: number;
|
|
150
|
+
column?: number;
|
|
151
|
+
help: string;
|
|
152
|
+
docsUrl: string;
|
|
153
|
+
suggestion?: string;
|
|
154
|
+
}
|
|
155
|
+
export interface ValidationResult {
|
|
156
|
+
valid: boolean;
|
|
157
|
+
errors: ValidationError[];
|
|
158
|
+
warnings: ValidationError[];
|
|
159
|
+
}
|
|
160
|
+
export type { Har, Entry, Header };
|
|
161
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAIpD,MAAM,MAAM,YAAY,GACpB,UAAU,GACV,QAAQ,GACR,YAAY,GACZ,OAAO,GACP,MAAM,GACN,OAAO,GACP,KAAK,GACL,OAAO,GACP,WAAW,GACX,OAAO,CAAA;AAEX,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,yBAAyB;IACzB,KAAK,EAAE,KAAK,CAAA;IACZ,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAA;IACnB,8DAA8D;IAC9D,aAAa,EAAE,MAAM,CAAA;IACrB,6EAA6E;IAC7E,oBAAoB,EAAE,MAAM,CAAA;IAC5B,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAA;IACnB,2CAA2C;IAC3C,OAAO,EAAE,iBAAiB,CAAA;IAC1B,6BAA6B;IAC7B,YAAY,EAAE,YAAY,CAAA;IAC1B,4EAA4E;IAC5E,UAAU,EAAE,OAAO,CAAA;IACnB,6CAA6C;IAC7C,WAAW,EAAE,OAAO,CAAA;IACpB,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAA;CACpB;AAID,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,CAAA;AAE3D,MAAM,MAAM,aAAa,GACrB,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,cAAc,GACd,UAAU,GACV,QAAQ,GACR,eAAe,GACf,aAAa,CAAA;AAEjB,MAAM,WAAW,OAAO;IACtB,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAA;IACd,qBAAqB;IACrB,QAAQ,EAAE,aAAa,CAAA;IACvB,wCAAwC;IACxC,QAAQ,EAAE,aAAa,CAAA;IACvB,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAA;IACb,0CAA0C;IAC1C,WAAW,EAAE,MAAM,CAAA;IACnB,wCAAwC;IACxC,cAAc,EAAE,MAAM,CAAA;IACtB,6CAA6C;IAC7C,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,4BAA4B;IAC5B,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,MAAM,CAAA;IACtB,iBAAiB,EAAE,MAAM,CAAA;IACzB,cAAc,EAAE,MAAM,CAAA;IACtB,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,yBAAyB;IACzB,OAAO,EAAE,eAAe,EAAE,CAAA;IAC1B,wBAAwB;IACxB,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,gCAAgC;IAChC,SAAS,EAAE,eAAe,CAAA;IAC1B,qDAAqD;IACrD,QAAQ,EAAE,eAAe,EAAE,CAAA;IAC3B,wBAAwB;IACxB,QAAQ,EAAE,gBAAgB,CAAA;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,0CAA0C;IAC1C,eAAe,CAAC,EAAE,OAAO,EAAE,CAAA;IAC3B,6BAA6B;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,8CAA8C;IAC9C,WAAW,CAAC,EAAE,aAAa,CAAA;CAC5B;AAID,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,iBAAiB,EAAE,cAAc,EAAE,CAAA;IACnC,aAAa,EAAE,MAAM,CAAA;IACrB,aAAa,EAAE,MAAM,CAAA;IACrB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,eAAe,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,cAAc,CAAA;CAC1B;AAID,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,aAAa,CAAA;IAC7B,aAAa,EAAE,aAAa,CAAA;IAC5B,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,OAAO,EAAE,CAAA;IACtB,gBAAgB,EAAE,OAAO,EAAE,CAAA;IAC3B,iBAAiB,EAAE,YAAY,EAAE,CAAA;IACjC,YAAY,EAAE,WAAW,EAAE,CAAA;IAC3B,iBAAiB,EAAE,MAAM,CAAA;IACzB,cAAc,EAAE,MAAM,CAAA;CACvB;AAID,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,WAAW,CAAA;AAErD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,YAAY,CAAA;IAClB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,gBAAgB,EAAE,CAAA;CAChC;AAED,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,cAAc,GACd,cAAc,GACd,kBAAkB,GAClB,gBAAgB,GAChB,cAAc,CAAA;AAIlB,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAA;IACd,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,QAAQ,EAAE,eAAe,EAAE,CAAA;CAC5B;AAID,YAAY,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unbatched API detection: TypeScript rule (not YAML-expressible).
|
|
3
|
+
* Groups requests by normalized URL path, flags 5+ requests in a 2-second window.
|
|
4
|
+
*/
|
|
5
|
+
import type { NormalizedEntry, Finding } from './types.js';
|
|
6
|
+
export declare function detectUnbatchedApis(entries: NormalizedEntry[]): Finding | null;
|
|
7
|
+
//# sourceMappingURL=unbatched-detect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unbatched-detect.d.ts","sourceRoot":"","sources":["../../src/lib/unbatched-detect.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAM1D,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,OAAO,GAAG,IAAI,CAgE9E"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { normalizeUrlForGrouping } from './diff.js';
|
|
2
|
+
const MIN_COUNT = 5;
|
|
3
|
+
const WINDOW_MS = 2000;
|
|
4
|
+
export function detectUnbatchedApis(entries) {
|
|
5
|
+
// Group by normalized URL pattern
|
|
6
|
+
const groups = new Map();
|
|
7
|
+
for (let i = 0; i < entries.length; i++) {
|
|
8
|
+
const entry = entries[i];
|
|
9
|
+
if (entry.isWebSocket || entry.isLongPoll)
|
|
10
|
+
continue;
|
|
11
|
+
const url = entry.entry.request?.url;
|
|
12
|
+
if (!url)
|
|
13
|
+
continue;
|
|
14
|
+
const pattern = normalizeUrlForGrouping(url);
|
|
15
|
+
const existing = groups.get(pattern);
|
|
16
|
+
if (existing) {
|
|
17
|
+
existing.push(i);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
groups.set(pattern, [i]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// For each group with 5+ entries, check time-window clustering
|
|
24
|
+
const affectedIndices = [];
|
|
25
|
+
for (const [, indices] of groups) {
|
|
26
|
+
if (indices.length < MIN_COUNT)
|
|
27
|
+
continue;
|
|
28
|
+
// Sort by start time
|
|
29
|
+
const sorted = [...indices].sort((a, b) => entries[a].startTimeMs - entries[b].startTimeMs);
|
|
30
|
+
// Sliding window: find clusters of MIN_COUNT+ within WINDOW_MS
|
|
31
|
+
let windowStart = 0;
|
|
32
|
+
for (let windowEnd = 0; windowEnd < sorted.length; windowEnd++) {
|
|
33
|
+
while (entries[sorted[windowEnd]].startTimeMs - entries[sorted[windowStart]].startTimeMs > WINDOW_MS) {
|
|
34
|
+
windowStart++;
|
|
35
|
+
}
|
|
36
|
+
if (windowEnd - windowStart + 1 >= MIN_COUNT) {
|
|
37
|
+
// Add all entries in this cluster
|
|
38
|
+
for (let j = windowStart; j <= windowEnd; j++) {
|
|
39
|
+
if (!affectedIndices.includes(sorted[j])) {
|
|
40
|
+
affectedIndices.push(sorted[j]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (affectedIndices.length < MIN_COUNT)
|
|
47
|
+
return null;
|
|
48
|
+
const count = affectedIndices.length;
|
|
49
|
+
return {
|
|
50
|
+
ruleId: 'unbatched-api-calls',
|
|
51
|
+
category: 'performance',
|
|
52
|
+
severity: count >= 10 ? 'critical' : 'warning',
|
|
53
|
+
title: `${count} unbatched API call${count !== 1 ? 's' : ''}`,
|
|
54
|
+
description: `${count} requests to similar API endpoints were made within short time windows. Batching these requests would reduce round-trips and improve performance.`,
|
|
55
|
+
recommendation: 'Batch multiple API calls into a single request where the API supports it. Use debouncing or request coalescing for repeated similar calls.',
|
|
56
|
+
affectedEntries: affectedIndices,
|
|
57
|
+
impact: 0,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/lib/validator.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,gBAAgB,EAAmB,MAAM,YAAY,CAAA;AACnE,OAAO,KAAK,EAMV,oBAAoB,EACrB,MAAM,aAAa,CAAA;AA8RpB,wBAAgB,QAAQ,CACtB,WAAW,EAAE,MAAM,EACnB,gBAAgB,CAAC,EAAE,oBAAoB,GACtC,gBAAgB,CA+KlB"}
|