evershield 0.0.0-stage → 0.2.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 +215 -2
- package/dist/audit/http.d.ts +25 -0
- package/dist/audit/http.js +98 -0
- package/dist/audit/json-paths.d.ts +20 -0
- package/dist/audit/json-paths.js +60 -0
- package/dist/audit/rules.d.ts +31 -0
- package/dist/audit/rules.js +158 -0
- package/dist/audit/run.d.ts +25 -0
- package/dist/audit/run.js +62 -0
- package/dist/audit/types.d.ts +57 -0
- package/dist/audit/types.js +10 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +8 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +213 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.js +352 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +8 -0
- package/dist/init.d.ts +21 -0
- package/dist/init.js +136 -0
- package/dist/report/index.d.ts +19 -0
- package/dist/report/index.js +82 -0
- package/dist/report/sarif.d.ts +2 -0
- package/dist/report/sarif.js +129 -0
- package/dist/rules/index.d.ts +4 -0
- package/dist/rules/index.js +11 -0
- package/dist/rules/internal-endpoint.d.ts +2 -0
- package/dist/rules/internal-endpoint.js +61 -0
- package/dist/rules/secret-like-string.d.ts +2 -0
- package/dist/rules/secret-like-string.js +128 -0
- package/dist/rules/sensitive-env-name.d.ts +4 -0
- package/dist/rules/sensitive-env-name.js +76 -0
- package/dist/rules/source-map-published.d.ts +2 -0
- package/dist/rules/source-map-published.js +39 -0
- package/dist/rules/types.d.ts +23 -0
- package/dist/rules/types.js +6 -0
- package/dist/scan/adapters.d.ts +24 -0
- package/dist/scan/adapters.js +65 -0
- package/dist/scan/artifact.d.ts +18 -0
- package/dist/scan/artifact.js +68 -0
- package/dist/scan/files.d.ts +16 -0
- package/dist/scan/files.js +41 -0
- package/dist/scan/text.d.ts +18 -0
- package/dist/scan/text.js +58 -0
- package/dist/surface/collect.d.ts +13 -0
- package/dist/surface/collect.js +130 -0
- package/dist/surface/driver.d.ts +17 -0
- package/dist/surface/driver.js +114 -0
- package/dist/surface/rules.d.ts +32 -0
- package/dist/surface/rules.js +150 -0
- package/dist/surface/run.d.ts +23 -0
- package/dist/surface/run.js +80 -0
- package/dist/surface/types.d.ts +90 -0
- package/dist/surface/types.js +10 -0
- package/package.json +73 -4
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ScanSummary } from "../index.ts";
|
|
2
|
+
import { type SurfaceDriver } from "./driver.ts";
|
|
3
|
+
import type { SurfaceConfig } from "./types.ts";
|
|
4
|
+
export interface SurfaceResult extends ScanSummary {
|
|
5
|
+
layer: "surface";
|
|
6
|
+
target: string;
|
|
7
|
+
identities: string[];
|
|
8
|
+
pagesAudited: number;
|
|
9
|
+
/** Page loads that rendered and were evaluated by the rules. */
|
|
10
|
+
snapshotsEvaluated: number;
|
|
11
|
+
ignored: number;
|
|
12
|
+
warnings: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface SurfaceOptions {
|
|
15
|
+
env?: NodeJS.ProcessEnv;
|
|
16
|
+
/** Injected in tests; defaults to a Playwright Chromium. */
|
|
17
|
+
driver?: SurfaceDriver;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Layer 3: open each page as each identity, least privileged first, one at a
|
|
21
|
+
* time, and run the surface rules over what each identity's browser held.
|
|
22
|
+
*/
|
|
23
|
+
export declare function runSurface(config: SurfaceConfig, options?: SurfaceOptions): Promise<SurfaceResult>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { VERSION } from "../index.js";
|
|
2
|
+
import { sortFindings } from "../scan/artifact.js";
|
|
3
|
+
import { launchPlaywrightDriver } from "./driver.js";
|
|
4
|
+
import { SURFACE_RULES } from "./rules.js";
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
6
|
+
const DEFAULT_MIN_TEXT = 12;
|
|
7
|
+
/**
|
|
8
|
+
* Layer 3: open each page as each identity, least privileged first, one at a
|
|
9
|
+
* time, and run the surface rules over what each identity's browser held.
|
|
10
|
+
*/
|
|
11
|
+
export async function runSurface(config, options = {}) {
|
|
12
|
+
const startedAt = new Date().toISOString();
|
|
13
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
14
|
+
const minTextLength = config.minTextLength ?? DEFAULT_MIN_TEXT;
|
|
15
|
+
const findings = [];
|
|
16
|
+
const warnings = [];
|
|
17
|
+
let snapshotsEvaluated = 0;
|
|
18
|
+
const driver = options.driver ?? (await launchPlaywrightDriver(options.env));
|
|
19
|
+
try {
|
|
20
|
+
for (const page of config.pages) {
|
|
21
|
+
const url = new URL(page.path, config.baseUrl).toString();
|
|
22
|
+
const observation = { page, url, tiers: config.tiers, snapshots: new Map(), minTextLength };
|
|
23
|
+
for (const identityName of config.tiers) {
|
|
24
|
+
const identity = config.identities[identityName];
|
|
25
|
+
if (identity === undefined) {
|
|
26
|
+
warnings.push(`${page.path}: identity "${identityName}" is not defined`);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const snapshot = await driver.capture({
|
|
30
|
+
baseUrl: config.baseUrl,
|
|
31
|
+
url,
|
|
32
|
+
identityName,
|
|
33
|
+
identity,
|
|
34
|
+
absentSelectors: page.expect?.[identityName]?.absentSelectors ?? [],
|
|
35
|
+
timeoutMs,
|
|
36
|
+
...(page.waitFor === undefined ? {} : { waitFor: page.waitFor }),
|
|
37
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
38
|
+
});
|
|
39
|
+
if (snapshot.error === undefined && new URL(snapshot.finalUrl).origin !== new URL(config.baseUrl).origin) {
|
|
40
|
+
snapshot.error = `redirected off the audited origin to ${new URL(snapshot.finalUrl).origin}`;
|
|
41
|
+
}
|
|
42
|
+
observation.snapshots.set(identityName, snapshot);
|
|
43
|
+
for (const match of snapshot.selectorMatches) {
|
|
44
|
+
if (match.text === "(invalid selector)")
|
|
45
|
+
warnings.push(`${page.path}: absentSelectors entry "${match.selector}" is not a valid CSS selector`);
|
|
46
|
+
}
|
|
47
|
+
if (snapshot.error !== undefined) {
|
|
48
|
+
warnings.push(`${page.path} as ${identityName}: ${snapshot.error}, not evaluated`);
|
|
49
|
+
}
|
|
50
|
+
else if (snapshot.status >= 400) {
|
|
51
|
+
warnings.push(`${page.path} as ${identityName}: HTTP ${snapshot.status}, not evaluated`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
snapshotsEvaluated += 1;
|
|
55
|
+
if (snapshot.finalUrl !== url)
|
|
56
|
+
warnings.push(`${page.path} as ${identityName}: landed on ${snapshot.finalUrl}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const rule of SURFACE_RULES)
|
|
60
|
+
findings.push(...rule.check(observation));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
if (options.driver === undefined)
|
|
65
|
+
await driver.close();
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
version: VERSION,
|
|
69
|
+
layer: "surface",
|
|
70
|
+
target: config.baseUrl,
|
|
71
|
+
identities: [...config.tiers],
|
|
72
|
+
pagesAudited: config.pages.length,
|
|
73
|
+
snapshotsEvaluated,
|
|
74
|
+
ignored: 0,
|
|
75
|
+
warnings,
|
|
76
|
+
startedAt,
|
|
77
|
+
finishedAt: new Date().toISOString(),
|
|
78
|
+
findings: sortFindings(findings),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 3: rendered-surface audit.
|
|
3
|
+
*
|
|
4
|
+
* The operator lists pages and identities. evershield opens each page in a
|
|
5
|
+
* headless browser as each identity, waits for it to settle, and records
|
|
6
|
+
* every text-bearing element with whether a person can actually see it,
|
|
7
|
+
* plus `data-*` attributes, inline scripts and web storage. The rules then
|
|
8
|
+
* look for data that is present but only visually hidden.
|
|
9
|
+
*/
|
|
10
|
+
export interface SurfaceIdentityConfig {
|
|
11
|
+
/** Sent only to requests for `baseUrl`'s origin. Credential headers must reference env vars. */
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
/** Cookie name → value, set for `baseUrl`. Values must reference env vars. */
|
|
14
|
+
cookies?: Record<string, string>;
|
|
15
|
+
/** Path to a Playwright storage-state file (cookies + localStorage), e.g. from `playwright codegen --save-storage`. May reference env vars. */
|
|
16
|
+
storageState?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface PageExpectation {
|
|
19
|
+
/** Strings that must not appear anywhere for this identity: DOM text (hidden or not), attributes, inline scripts, storage. */
|
|
20
|
+
absentText?: string[];
|
|
21
|
+
/** CSS selectors that must match nothing for this identity. */
|
|
22
|
+
absentSelectors?: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface PageConfig {
|
|
25
|
+
/** Path relative to `baseUrl`, starting with `/`. */
|
|
26
|
+
path: string;
|
|
27
|
+
/** CSS selector to wait for before capturing, for pages that render after data loads. */
|
|
28
|
+
waitFor?: string;
|
|
29
|
+
expect?: Record<string, PageExpectation>;
|
|
30
|
+
}
|
|
31
|
+
export interface SurfaceConfig {
|
|
32
|
+
/** Origin the browser is allowed to be credentialed for. Pages are relative to it. */
|
|
33
|
+
baseUrl: string;
|
|
34
|
+
/** Identity names ordered from least to most privileged. */
|
|
35
|
+
tiers: string[];
|
|
36
|
+
identities: Record<string, SurfaceIdentityConfig>;
|
|
37
|
+
pages: PageConfig[];
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
/** Shortest text, in characters, the differential rule compares. Default 12. */
|
|
40
|
+
minTextLength?: number;
|
|
41
|
+
}
|
|
42
|
+
/** Why an element cannot be seen. `null` means visible. */
|
|
43
|
+
export type HiddenBy = "display" | "visibility" | "opacity" | "blur" | "transparent-text" | "clipped" | "zero-size";
|
|
44
|
+
export interface TextBlock {
|
|
45
|
+
selector: string;
|
|
46
|
+
/** Element's own text (direct text nodes), whitespace collapsed. */
|
|
47
|
+
text: string;
|
|
48
|
+
hiddenBy: HiddenBy | null;
|
|
49
|
+
}
|
|
50
|
+
export interface AttributeValue {
|
|
51
|
+
selector: string;
|
|
52
|
+
name: string;
|
|
53
|
+
value: string;
|
|
54
|
+
}
|
|
55
|
+
export interface ScriptText {
|
|
56
|
+
selector: string;
|
|
57
|
+
text: string;
|
|
58
|
+
}
|
|
59
|
+
export interface StorageEntry {
|
|
60
|
+
area: "localStorage" | "sessionStorage";
|
|
61
|
+
key: string;
|
|
62
|
+
value: string;
|
|
63
|
+
}
|
|
64
|
+
export interface SurfaceSnapshot {
|
|
65
|
+
identity: string;
|
|
66
|
+
/** URL requested. */
|
|
67
|
+
url: string;
|
|
68
|
+
/** URL after redirects. */
|
|
69
|
+
finalUrl: string;
|
|
70
|
+
status: number;
|
|
71
|
+
blocks: TextBlock[];
|
|
72
|
+
attributes: AttributeValue[];
|
|
73
|
+
scripts: ScriptText[];
|
|
74
|
+
storage: StorageEntry[];
|
|
75
|
+
/** Selectors from the page's `absentSelectors` expectations that matched, with a text sample. */
|
|
76
|
+
selectorMatches: Array<{
|
|
77
|
+
selector: string;
|
|
78
|
+
text: string;
|
|
79
|
+
}>;
|
|
80
|
+
/** Navigation or capture failure. */
|
|
81
|
+
error?: string;
|
|
82
|
+
}
|
|
83
|
+
export interface PageObservation {
|
|
84
|
+
page: PageConfig;
|
|
85
|
+
/** Absolute URL of the page. */
|
|
86
|
+
url: string;
|
|
87
|
+
tiers: string[];
|
|
88
|
+
snapshots: Map<string, SurfaceSnapshot>;
|
|
89
|
+
minTextLength: number;
|
|
90
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 3: rendered-surface audit.
|
|
3
|
+
*
|
|
4
|
+
* The operator lists pages and identities. evershield opens each page in a
|
|
5
|
+
* headless browser as each identity, waits for it to settle, and records
|
|
6
|
+
* every text-bearing element with whether a person can actually see it,
|
|
7
|
+
* plus `data-*` attributes, inline scripts and web storage. The rules then
|
|
8
|
+
* look for data that is present but only visually hidden.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,75 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evershield",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Shield everything, everywhere: find what your web app exposes to the browser, in bundles, API responses and the rendered page",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Lee Dongwook (@inviy)",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/inviy/evershield.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/inviy/evershield#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/inviy/evershield/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"security",
|
|
17
|
+
"exposure",
|
|
18
|
+
"client-side",
|
|
19
|
+
"bundle",
|
|
20
|
+
"sourcemap",
|
|
21
|
+
"secrets",
|
|
22
|
+
"authorization",
|
|
23
|
+
"paywall",
|
|
24
|
+
"static-analysis",
|
|
25
|
+
"sarif",
|
|
26
|
+
"ci",
|
|
27
|
+
"devsecops",
|
|
28
|
+
"playwright",
|
|
29
|
+
"dom"
|
|
30
|
+
],
|
|
31
|
+
"type": "module",
|
|
32
|
+
"bin": {
|
|
33
|
+
"evershield": "dist/bin.js"
|
|
34
|
+
},
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"import": "./dist/index.js"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE"
|
|
45
|
+
],
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=22.12"
|
|
48
|
+
},
|
|
49
|
+
"packageManager": "pnpm@10.28.0",
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsc -p tsconfig.build.json",
|
|
52
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
53
|
+
"test": "node --experimental-strip-types --test \"src/**/*.test.ts\"",
|
|
54
|
+
"check": "pnpm typecheck && pnpm test && pnpm build",
|
|
55
|
+
"prepublishOnly": "pnpm check"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "^22.15.0",
|
|
59
|
+
"playwright": "1.56.1",
|
|
60
|
+
"playwright-core": "1.56.1",
|
|
61
|
+
"typescript": "^5.8.0"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"playwright": ">=1.40.0",
|
|
65
|
+
"playwright-core": ">=1.40.0"
|
|
66
|
+
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"playwright": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"playwright-core": {
|
|
72
|
+
"optional": true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|