@prism-d1/cli 1.1.2 → 1.3.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/dist/assets/github-workflows/prism-agent-eval.yml +2 -1
- package/dist/assets/github-workflows/prism-eval-gate-kiro.yml +12 -2
- package/dist/assets/gitlab-workflows/prism-eval-gate.yml +8 -0
- package/dist/assets/infra/bin/app.ts +61 -37
- package/dist/assets/infra/lib/constructs/otel-collector-construct.ts +96 -0
- package/dist/assets/infra/lib/constructs/security-agent-construct.ts +35 -30
- package/dist/assets/infra/lib/dashboard-stack.ts +27 -0
- package/dist/assets/infra/lib/lambda/otel-receiver.ts +278 -8
- package/dist/assets/infra/lib/lambda/pricing/litellm-snapshot.json +3769 -0
- package/dist/assets/infra/lib/lambda/pricing/price-source.ts +56 -0
- package/dist/assets/infra/lib/lambda/pricing/price-table.ts +240 -0
- package/dist/assets/infra/lib/lambda/pricing/prices-key.ts +6 -0
- package/dist/assets/infra/lib/lambda/pricing/reprice.ts +108 -0
- package/dist/assets/infra/lib/lambda/pricing-refresh.ts +352 -0
- package/dist/assets/infra/lib/lambda/velocity-widget.ts +124 -1
- package/dist/assets/infra/lib/metrics-pipeline-stack.ts +2 -110
- package/dist/assets/infra/lib/security-agent-resources.ts +136 -0
- package/dist/assets/infra/lib/security-agent-stack.ts +69 -0
- package/dist/assets/infra/package.json +4 -1
- package/dist/assets/metric-hooks/prepare-commit-msg +3 -1
- package/dist/assets/pentest-cdk/bin/app.ts +12 -0
- package/dist/assets/pentest-cdk/cdk.json +7 -0
- package/dist/assets/pentest-cdk/lib/pentest-target-stack.ts +206 -0
- package/dist/assets/pentest-cdk/package.json +18 -0
- package/dist/assets/pentest-cdk/tsconfig.json +12 -0
- package/dist/src/codewatch/config.d.ts +17 -0
- package/dist/src/codewatch/config.d.ts.map +1 -0
- package/dist/src/codewatch/config.js +40 -0
- package/dist/src/codewatch/config.js.map +1 -0
- package/dist/src/codewatch/gitleaks.d.ts +49 -0
- package/dist/src/codewatch/gitleaks.d.ts.map +1 -0
- package/dist/src/codewatch/gitleaks.js +160 -0
- package/dist/src/codewatch/gitleaks.js.map +1 -0
- package/dist/src/codewatch/globalHooks.d.ts +52 -0
- package/dist/src/codewatch/globalHooks.d.ts.map +1 -0
- package/dist/src/codewatch/globalHooks.js +235 -0
- package/dist/src/codewatch/globalHooks.js.map +1 -0
- package/dist/src/codewatch/hooks.d.ts +38 -0
- package/dist/src/codewatch/hooks.d.ts.map +1 -0
- package/dist/src/codewatch/hooks.js +176 -0
- package/dist/src/codewatch/hooks.js.map +1 -0
- package/dist/src/codewatch/prepush.d.ts +37 -0
- package/dist/src/codewatch/prepush.d.ts.map +1 -0
- package/dist/src/codewatch/prepush.js +231 -0
- package/dist/src/codewatch/prepush.js.map +1 -0
- package/dist/src/codewatch/results.d.ts +210 -0
- package/dist/src/codewatch/results.d.ts.map +1 -0
- package/dist/src/codewatch/results.js +127 -0
- package/dist/src/codewatch/results.js.map +1 -0
- package/dist/src/codewatch/reviewers/claude.d.ts +9 -0
- package/dist/src/codewatch/reviewers/claude.d.ts.map +1 -0
- package/dist/src/codewatch/reviewers/claude.js +48 -0
- package/dist/src/codewatch/reviewers/claude.js.map +1 -0
- package/dist/src/codewatch/reviewers/index.d.ts +39 -0
- package/dist/src/codewatch/reviewers/index.d.ts.map +1 -0
- package/dist/src/codewatch/reviewers/index.js +97 -0
- package/dist/src/codewatch/reviewers/index.js.map +1 -0
- package/dist/src/codewatch/reviewers/kiro.d.ts +14 -0
- package/dist/src/codewatch/reviewers/kiro.d.ts.map +1 -0
- package/dist/src/codewatch/reviewers/kiro.js +32 -0
- package/dist/src/codewatch/reviewers/kiro.js.map +1 -0
- package/dist/src/codewatch/reviewers/types.d.ts +56 -0
- package/dist/src/codewatch/reviewers/types.d.ts.map +1 -0
- package/dist/src/codewatch/reviewers/types.js +91 -0
- package/dist/src/codewatch/reviewers/types.js.map +1 -0
- package/dist/src/codewatch/rules.d.ts +12 -0
- package/dist/src/codewatch/rules.d.ts.map +1 -0
- package/dist/src/codewatch/rules.js +27 -0
- package/dist/src/codewatch/rules.js.map +1 -0
- package/dist/src/commands/_top/dashboard.d.ts +17 -0
- package/dist/src/commands/_top/dashboard.d.ts.map +1 -0
- package/dist/src/commands/_top/dashboard.js +76 -0
- package/dist/src/commands/_top/dashboard.js.map +1 -0
- package/dist/src/commands/_top/version.d.ts +62 -0
- package/dist/src/commands/_top/version.d.ts.map +1 -0
- package/dist/src/commands/_top/version.js +175 -0
- package/dist/src/commands/_top/version.js.map +1 -0
- package/dist/src/commands/bedrock-protection/scan-account.d.ts.map +1 -1
- package/dist/src/commands/bedrock-protection/scan-account.js +2 -1
- package/dist/src/commands/bedrock-protection/scan-account.js.map +1 -1
- package/dist/src/commands/bedrock-protection/scan-org.d.ts.map +1 -1
- package/dist/src/commands/bedrock-protection/scan-org.js +16 -4
- package/dist/src/commands/bedrock-protection/scan-org.js.map +1 -1
- package/dist/src/commands/bootstrapper/install-eval-harness.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/install-eval-harness.js +15 -48
- package/dist/src/commands/bootstrapper/install-eval-harness.js.map +1 -1
- package/dist/src/commands/bootstrapper/install-git-hooks.d.ts +13 -0
- package/dist/src/commands/bootstrapper/install-git-hooks.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/install-git-hooks.js +28 -147
- package/dist/src/commands/bootstrapper/install-git-hooks.js.map +1 -1
- package/dist/src/commands/bootstrapper/setup-otel-sync.d.ts +12 -6
- package/dist/src/commands/bootstrapper/setup-otel-sync.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/setup-otel-sync.js +18 -443
- package/dist/src/commands/bootstrapper/setup-otel-sync.js.map +1 -1
- package/dist/src/commands/codewatch/pre-commit.d.ts +7 -0
- package/dist/src/commands/codewatch/pre-commit.d.ts.map +1 -0
- package/dist/src/commands/codewatch/pre-commit.js +30 -0
- package/dist/src/commands/codewatch/pre-commit.js.map +1 -0
- package/dist/src/commands/codewatch/pre-push.d.ts +16 -0
- package/dist/src/commands/codewatch/pre-push.d.ts.map +1 -0
- package/dist/src/commands/codewatch/pre-push.js +32 -0
- package/dist/src/commands/codewatch/pre-push.js.map +1 -0
- package/dist/src/commands/codewatch/report.d.ts +22 -0
- package/dist/src/commands/codewatch/report.d.ts.map +1 -0
- package/dist/src/commands/codewatch/report.js +64 -0
- package/dist/src/commands/codewatch/report.js.map +1 -0
- package/dist/src/commands/enable/codeburn.d.ts +59 -0
- package/dist/src/commands/enable/codeburn.d.ts.map +1 -0
- package/dist/src/commands/enable/codeburn.js +531 -0
- package/dist/src/commands/enable/codeburn.js.map +1 -0
- package/dist/src/commands/enable/codewatch.d.ts +15 -0
- package/dist/src/commands/enable/codewatch.d.ts.map +1 -0
- package/dist/src/commands/enable/codewatch.js +186 -0
- package/dist/src/commands/enable/codewatch.js.map +1 -0
- package/dist/src/commands/enable/security.d.ts +41 -0
- package/dist/src/commands/enable/security.d.ts.map +1 -0
- package/dist/src/commands/enable/security.js +130 -0
- package/dist/src/commands/enable/security.js.map +1 -0
- package/dist/src/commands/export/otel.d.ts +31 -0
- package/dist/src/commands/export/otel.d.ts.map +1 -0
- package/dist/src/commands/export/otel.js +52 -0
- package/dist/src/commands/export/otel.js.map +1 -0
- package/dist/src/commands/git/claude-session-context.d.ts.map +1 -1
- package/dist/src/commands/git/claude-session-context.js +2 -1
- package/dist/src/commands/git/claude-session-context.js.map +1 -1
- package/dist/src/commands/git/commit-trailers.js +1 -1
- package/dist/src/commands/git/commit-trailers.js.map +1 -1
- package/dist/src/commands/security/code-review.d.ts +22 -0
- package/dist/src/commands/security/code-review.d.ts.map +1 -0
- package/dist/src/commands/security/code-review.js +43 -0
- package/dist/src/commands/security/code-review.js.map +1 -0
- package/dist/src/commands/security/findings.d.ts +15 -0
- package/dist/src/commands/security/findings.d.ts.map +1 -0
- package/dist/src/commands/security/findings.js +31 -0
- package/dist/src/commands/security/findings.js.map +1 -0
- package/dist/src/commands/security/pentest.d.ts +18 -0
- package/dist/src/commands/security/pentest.d.ts.map +1 -0
- package/dist/src/commands/security/pentest.js +32 -0
- package/dist/src/commands/security/pentest.js.map +1 -0
- package/dist/src/commands/security/threat-model.d.ts +16 -0
- package/dist/src/commands/security/threat-model.d.ts.map +1 -0
- package/dist/src/commands/security/threat-model.js +24 -0
- package/dist/src/commands/security/threat-model.js.map +1 -0
- package/dist/src/commands/security/threats.d.ts +12 -0
- package/dist/src/commands/security/threats.d.ts.map +1 -0
- package/dist/src/commands/security/threats.js +23 -0
- package/dist/src/commands/security/threats.js.map +1 -0
- package/dist/src/commands/securityagent/setup.d.ts +19 -5
- package/dist/src/commands/securityagent/setup.d.ts.map +1 -1
- package/dist/src/commands/securityagent/setup.js +25 -189
- package/dist/src/commands/securityagent/setup.js.map +1 -1
- package/dist/src/commands/workshop/verify-setup.d.ts.map +1 -1
- package/dist/src/commands/workshop/verify-setup.js +5 -3
- package/dist/src/commands/workshop/verify-setup.js.map +1 -1
- package/dist/src/dashboard/codewatch.d.ts +106 -0
- package/dist/src/dashboard/codewatch.d.ts.map +1 -0
- package/dist/src/dashboard/codewatch.js +120 -0
- package/dist/src/dashboard/codewatch.js.map +1 -0
- package/dist/src/dashboard/commits.d.ts +96 -0
- package/dist/src/dashboard/commits.d.ts.map +1 -0
- package/dist/src/dashboard/commits.js +207 -0
- package/dist/src/dashboard/commits.js.map +1 -0
- package/dist/src/dashboard/page.d.ts +10 -0
- package/dist/src/dashboard/page.d.ts.map +1 -0
- package/dist/src/dashboard/page.js +470 -0
- package/dist/src/dashboard/page.js.map +1 -0
- package/dist/src/dashboard/payload.d.ts +114 -0
- package/dist/src/dashboard/payload.d.ts.map +1 -0
- package/dist/src/dashboard/payload.js +96 -0
- package/dist/src/dashboard/payload.js.map +1 -0
- package/dist/src/dashboard/report.d.ts +99 -0
- package/dist/src/dashboard/report.d.ts.map +1 -0
- package/dist/src/dashboard/report.js +300 -0
- package/dist/src/dashboard/report.js.map +1 -0
- package/dist/src/dashboard/repos.d.ts +58 -0
- package/dist/src/dashboard/repos.d.ts.map +1 -0
- package/dist/src/dashboard/repos.js +111 -0
- package/dist/src/dashboard/repos.js.map +1 -0
- package/dist/src/dashboard/security.d.ts +130 -0
- package/dist/src/dashboard/security.d.ts.map +1 -0
- package/dist/src/dashboard/security.js +144 -0
- package/dist/src/dashboard/security.js.map +1 -0
- package/dist/src/dashboard/server.d.ts +52 -0
- package/dist/src/dashboard/server.d.ts.map +1 -0
- package/dist/src/dashboard/server.js +163 -0
- package/dist/src/dashboard/server.js.map +1 -0
- package/dist/src/dashboard/setup.d.ts +67 -0
- package/dist/src/dashboard/setup.d.ts.map +1 -0
- package/dist/src/dashboard/setup.js +146 -0
- package/dist/src/dashboard/setup.js.map +1 -0
- package/dist/src/dashboard/usage.d.ts +43 -0
- package/dist/src/dashboard/usage.d.ts.map +1 -0
- package/dist/src/dashboard/usage.js +88 -0
- package/dist/src/dashboard/usage.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +48 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/security/cfn-diff.d.ts +57 -0
- package/dist/src/security/cfn-diff.d.ts.map +1 -0
- package/dist/src/security/cfn-diff.js +112 -0
- package/dist/src/security/cfn-diff.js.map +1 -0
- package/dist/src/security/errors.d.ts +45 -0
- package/dist/src/security/errors.d.ts.map +1 -0
- package/dist/src/security/errors.js +116 -0
- package/dist/src/security/errors.js.map +1 -0
- package/dist/src/security/marker.d.ts +46 -0
- package/dist/src/security/marker.d.ts.map +1 -0
- package/dist/src/security/marker.js +120 -0
- package/dist/src/security/marker.js.map +1 -0
- package/dist/src/security/pentest-dockerfile.d.ts +8 -0
- package/dist/src/security/pentest-dockerfile.d.ts.map +1 -0
- package/dist/src/security/pentest-dockerfile.js +163 -0
- package/dist/src/security/pentest-dockerfile.js.map +1 -0
- package/dist/src/security/pentest.d.ts +233 -0
- package/dist/src/security/pentest.d.ts.map +1 -0
- package/dist/src/security/pentest.js +1057 -0
- package/dist/src/security/pentest.js.map +1 -0
- package/dist/src/security/repo.d.ts +25 -0
- package/dist/src/security/repo.d.ts.map +1 -0
- package/dist/src/security/repo.js +38 -0
- package/dist/src/security/repo.js.map +1 -0
- package/dist/src/security/review.d.ts +202 -0
- package/dist/src/security/review.d.ts.map +1 -0
- package/dist/src/security/review.js +706 -0
- package/dist/src/security/review.js.map +1 -0
- package/dist/src/security/setup.d.ts +193 -0
- package/dist/src/security/setup.d.ts.map +1 -0
- package/dist/src/security/setup.js +728 -0
- package/dist/src/security/setup.js.map +1 -0
- package/dist/src/security/threat-model.d.ts +133 -0
- package/dist/src/security/threat-model.d.ts.map +1 -0
- package/dist/src/security/threat-model.js +653 -0
- package/dist/src/security/threat-model.js.map +1 -0
- package/dist/src/utils/audit-bedrock.d.ts +1 -0
- package/dist/src/utils/audit-bedrock.d.ts.map +1 -1
- package/dist/src/utils/audit-bedrock.js +133 -0
- package/dist/src/utils/audit-bedrock.js.map +1 -1
- package/dist/src/utils/audit-org.d.ts +1 -0
- package/dist/src/utils/audit-org.d.ts.map +1 -1
- package/dist/src/utils/audit-org.js +155 -0
- package/dist/src/utils/audit-org.js.map +1 -1
- package/dist/src/utils/exec.d.ts +6 -0
- package/dist/src/utils/exec.d.ts.map +1 -1
- package/dist/src/utils/exec.js +12 -1
- package/dist/src/utils/exec.js.map +1 -1
- package/dist/src/utils/otel-sync-schedule.d.ts +41 -0
- package/dist/src/utils/otel-sync-schedule.d.ts.map +1 -0
- package/dist/src/utils/otel-sync-schedule.js +90 -0
- package/dist/src/utils/otel-sync-schedule.js.map +1 -0
- package/dist/src/utils/tool-check.d.ts +14 -0
- package/dist/src/utils/tool-check.d.ts.map +1 -0
- package/dist/src/utils/tool-check.js +42 -0
- package/dist/src/utils/tool-check.js.map +1 -0
- package/package.json +3 -2
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which repositories the dashboard shows.
|
|
3
|
+
*
|
|
4
|
+
* Default: every git repo under the user's home directory (3 levels deep),
|
|
5
|
+
* wherever the dashboard is started from, whether or not codewatch is set up
|
|
6
|
+
* in it -- a repo without codewatch is shown as missing. A repo is any
|
|
7
|
+
* directory with `.git`, as a directory or as a file (linked worktrees and
|
|
8
|
+
* submodules). The repo containing the working directory is listed first. `--root` moves the
|
|
9
|
+
* scan. Everything goes through discoverRepos() so a registry could be added
|
|
10
|
+
* later as one more source.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { basename, join, resolve } from 'node:path';
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
/**
|
|
17
|
+
* Directory names never descended into. Hidden names (leading `.`) are skipped
|
|
18
|
+
* separately, which covers `.git`, `.cache`, `.venv` and the like.
|
|
19
|
+
*/
|
|
20
|
+
export const SKIP_DIRS = new Set(['node_modules', 'Library', 'build', 'dist', 'target', 'venv', '__pycache__', 'cdk.out']);
|
|
21
|
+
export const DEFAULT_SCAN_DEPTH = 3;
|
|
22
|
+
export const MAX_REPOS = 200;
|
|
23
|
+
/** Hard cap on directories listed, so an unusually wide home cannot stall the page. */
|
|
24
|
+
export const MAX_DIRS_VISITED = 20_000;
|
|
25
|
+
export function gitToplevel(cwd) {
|
|
26
|
+
const r = spawnSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
27
|
+
return r.status === 0 && r.stdout.trim() ? r.stdout.trim() : null;
|
|
28
|
+
}
|
|
29
|
+
function real(p) {
|
|
30
|
+
try {
|
|
31
|
+
return realpathSync(p);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return resolve(p);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function isDir(p) {
|
|
38
|
+
try {
|
|
39
|
+
return statSync(p).isDirectory();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Git repos under `root`, at most `depth` levels below it. `.git` may be a
|
|
47
|
+
* directory or a file (a linked worktree's or submodule's pointer).
|
|
48
|
+
*
|
|
49
|
+
* - The walk stops at any git repo: nothing inside a repo is scanned, so
|
|
50
|
+
* nested repos and big working trees are skipped.
|
|
51
|
+
* - Hidden directories and SKIP_DIRS are never entered.
|
|
52
|
+
* - Symlinks are not followed (Dirent.isDirectory() is false for a link).
|
|
53
|
+
* - An unreadable directory is skipped, not an error.
|
|
54
|
+
* - `root` itself is never treated as a repo, so a home directory kept under
|
|
55
|
+
* git (dotfiles) does not end the scan before it starts.
|
|
56
|
+
*/
|
|
57
|
+
export function scanForRepos(root, depth = DEFAULT_SCAN_DEPTH) {
|
|
58
|
+
const repos = [];
|
|
59
|
+
let visited = 0;
|
|
60
|
+
let truncated = false;
|
|
61
|
+
const walk = (dir, level) => {
|
|
62
|
+
if (truncated)
|
|
63
|
+
return;
|
|
64
|
+
if (level > 0 && existsSync(join(dir, '.git'))) {
|
|
65
|
+
repos.push({ name: basename(dir), path: real(dir), current: false, hasPrism: isDir(join(dir, '.prism')) });
|
|
66
|
+
if (repos.length >= MAX_REPOS)
|
|
67
|
+
truncated = true;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (level >= depth)
|
|
71
|
+
return;
|
|
72
|
+
if (++visited > MAX_DIRS_VISITED) {
|
|
73
|
+
truncated = true;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let entries;
|
|
77
|
+
try {
|
|
78
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
84
|
+
for (const e of entries) {
|
|
85
|
+
if (!e.isDirectory() || e.name.startsWith('.') || SKIP_DIRS.has(e.name))
|
|
86
|
+
continue;
|
|
87
|
+
walk(join(dir, e.name), level + 1);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
walk(resolve(root), 0);
|
|
91
|
+
return { repos, visited, truncated };
|
|
92
|
+
}
|
|
93
|
+
export function discoverRepos(opts) {
|
|
94
|
+
const t0 = Date.now();
|
|
95
|
+
const top = gitToplevel(opts.cwd);
|
|
96
|
+
const currentRepo = top ? real(top) : null;
|
|
97
|
+
const scannedRoot = resolve(opts.root || opts.home || homedir());
|
|
98
|
+
const out = [];
|
|
99
|
+
if (currentRepo) {
|
|
100
|
+
out.push({ name: basename(currentRepo), path: currentRepo, current: true, hasPrism: isDir(join(currentRepo, '.prism')) });
|
|
101
|
+
}
|
|
102
|
+
if (!isDir(scannedRoot)) {
|
|
103
|
+
return { repos: out, scannedRoot, currentRepo, scanMs: Date.now() - t0, truncated: false, error: `scan root is not a directory: ${scannedRoot}` };
|
|
104
|
+
}
|
|
105
|
+
const scan = scanForRepos(scannedRoot);
|
|
106
|
+
for (const r of scan.repos)
|
|
107
|
+
if (r.path !== currentRepo)
|
|
108
|
+
out.push(r);
|
|
109
|
+
return { repos: out, scannedRoot, currentRepo, scanMs: Date.now() - t0, truncated: scan.truncated, error: null };
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=repos.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repos.js","sourceRoot":"","sources":["../../../src/dashboard/repos.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Security findings panel: the cached `security-review` results that
|
|
3
|
+
* `prism-cli security code-review` and `security findings` write to
|
|
4
|
+
* .prism/results/. Local files only; no AWS calls.
|
|
5
|
+
*/
|
|
6
|
+
import { type PrismResult } from '../codewatch/results.js';
|
|
7
|
+
export declare const MAX_PANEL_FINDINGS = 50;
|
|
8
|
+
/** A pending record older than this is marked `stale?`: results are local, so the dashboard cannot know the job's real state. */
|
|
9
|
+
export declare const PENDING_STALE_MS: number;
|
|
10
|
+
export declare const PENDING_HINT = "prism-cli security findings";
|
|
11
|
+
export interface SecurityFindingRow {
|
|
12
|
+
risk_level: string;
|
|
13
|
+
title: string;
|
|
14
|
+
file: string;
|
|
15
|
+
line: number | null;
|
|
16
|
+
cwe: string | null;
|
|
17
|
+
}
|
|
18
|
+
export interface SecurityRepoRow {
|
|
19
|
+
name: string;
|
|
20
|
+
path: string;
|
|
21
|
+
current: boolean;
|
|
22
|
+
job_id: string;
|
|
23
|
+
job_status: string;
|
|
24
|
+
status: PrismResult['status'];
|
|
25
|
+
scope: string;
|
|
26
|
+
started_at: string;
|
|
27
|
+
finished_at: string;
|
|
28
|
+
counts: Record<string, number>;
|
|
29
|
+
findings: SecurityFindingRow[];
|
|
30
|
+
truncated: boolean;
|
|
31
|
+
reason: string | null;
|
|
32
|
+
/** Started by code-review, no verdict recorded yet. */
|
|
33
|
+
pending: boolean;
|
|
34
|
+
/** Pending for longer than PENDING_STALE_MS: the job most likely finished and nobody fetched it. */
|
|
35
|
+
stale: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** A pentest runs for hours, so its pending record goes stale later than a review's. */
|
|
38
|
+
export declare const PENTEST_PENDING_STALE_MS: number;
|
|
39
|
+
export declare const PENTEST_HINT = "prism-cli security pentest --status";
|
|
40
|
+
export interface SecurityPentestFindingRow {
|
|
41
|
+
risk_level: string;
|
|
42
|
+
title: string;
|
|
43
|
+
confidence: string | null;
|
|
44
|
+
}
|
|
45
|
+
export interface SecurityPentestRow {
|
|
46
|
+
name: string;
|
|
47
|
+
path: string;
|
|
48
|
+
current: boolean;
|
|
49
|
+
job_id: string;
|
|
50
|
+
job_status: string;
|
|
51
|
+
status: PrismResult['status'];
|
|
52
|
+
target_host: string;
|
|
53
|
+
started_at: string;
|
|
54
|
+
finished_at: string;
|
|
55
|
+
counts: Record<string, number>;
|
|
56
|
+
findings: SecurityPentestFindingRow[];
|
|
57
|
+
truncated: boolean;
|
|
58
|
+
reason: string | null;
|
|
59
|
+
overview: string | null;
|
|
60
|
+
/** The last teardown left the stack up; --cleanup retries it. */
|
|
61
|
+
teardown_failed: {
|
|
62
|
+
at: string;
|
|
63
|
+
reason: string;
|
|
64
|
+
} | null;
|
|
65
|
+
pending: boolean;
|
|
66
|
+
stale: boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface SecurityPanel {
|
|
69
|
+
/** One row per repo with a cached code-review result, newest job first. */
|
|
70
|
+
repos: SecurityRepoRow[];
|
|
71
|
+
/** One row per repo with a cached penetration-test result, newest job first. */
|
|
72
|
+
pentests: SecurityPentestRow[];
|
|
73
|
+
hint: string;
|
|
74
|
+
/** Shown on a pending repo: how to fetch the job's results. */
|
|
75
|
+
pending_hint: string;
|
|
76
|
+
/** Shown on a pending pentest: how to refresh it. */
|
|
77
|
+
pentest_hint: string;
|
|
78
|
+
/** One row per repo with a cached threat model, current repo first. */
|
|
79
|
+
threat_models: ThreatModelRow[];
|
|
80
|
+
/** Shown on a pending threat model: how to fetch its threats. */
|
|
81
|
+
threats_hint: string;
|
|
82
|
+
}
|
|
83
|
+
/** Threats shown per repo; the rest are in the result file and `prism-cli security threats`. */
|
|
84
|
+
export declare const MAX_PANEL_THREATS = 5;
|
|
85
|
+
export declare const THREATS_HINT = "prism-cli security threats";
|
|
86
|
+
export interface ThreatModelRow {
|
|
87
|
+
name: string;
|
|
88
|
+
path: string;
|
|
89
|
+
current: boolean;
|
|
90
|
+
job_id: string;
|
|
91
|
+
job_status: string;
|
|
92
|
+
status: PrismResult['status'];
|
|
93
|
+
commit: string | null;
|
|
94
|
+
started_at: string;
|
|
95
|
+
finished_at: string;
|
|
96
|
+
counts: Record<string, number>;
|
|
97
|
+
scope_docs: string[];
|
|
98
|
+
/** The system overview's first paragraph. */
|
|
99
|
+
overview: string;
|
|
100
|
+
threats: {
|
|
101
|
+
severity: string;
|
|
102
|
+
title: string;
|
|
103
|
+
stride: string[];
|
|
104
|
+
}[];
|
|
105
|
+
/** Threats beyond MAX_PANEL_THREATS. */
|
|
106
|
+
more: number;
|
|
107
|
+
reason: string | null;
|
|
108
|
+
pending: boolean;
|
|
109
|
+
stale: boolean;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The latest job of a repo. Two files can hold the same job (code-review
|
|
113
|
+
* writes it with its scope, `security findings` again without); the one that
|
|
114
|
+
* knows its scope wins, then the newest write. A pending record is completed
|
|
115
|
+
* in place, but should a verdict for the same job exist elsewhere, it wins.
|
|
116
|
+
*/
|
|
117
|
+
export declare function latestSecurityReview(results: PrismResult[]): PrismResult | null;
|
|
118
|
+
/**
|
|
119
|
+
* The latest pentest job of a repo. Like latestSecurityReview: newest write per
|
|
120
|
+
* job, but a completed record for a job wins over its pending record.
|
|
121
|
+
*/
|
|
122
|
+
export declare function latestPentest(results: PrismResult[]): PrismResult | null;
|
|
123
|
+
export declare function collectSecurity(repos: {
|
|
124
|
+
name: string;
|
|
125
|
+
path: string;
|
|
126
|
+
current: boolean;
|
|
127
|
+
}[], read?: (root: string) => PrismResult[], now?: Date): SecurityPanel;
|
|
128
|
+
/** The newest threat model job of a repo; for one job, a verdict wins over a pending record. */
|
|
129
|
+
export declare function latestThreatModel(results: PrismResult[]): PrismResult | null;
|
|
130
|
+
//# sourceMappingURL=security.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../../../src/dashboard/security.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Security findings panel: the cached `security-review` results that
|
|
3
|
+
* `prism-cli security code-review` and `security findings` write to
|
|
4
|
+
* .prism/results/. Local files only; no AWS calls.
|
|
5
|
+
*/
|
|
6
|
+
import { readResults } from '../codewatch/results.js';
|
|
7
|
+
import { firstParagraph } from '../security/threat-model.js';
|
|
8
|
+
export const MAX_PANEL_FINDINGS = 50;
|
|
9
|
+
/** A pending record older than this is marked `stale?`: results are local, so the dashboard cannot know the job's real state. */
|
|
10
|
+
export const PENDING_STALE_MS = 2 * 60 * 60 * 1000;
|
|
11
|
+
export const PENDING_HINT = 'prism-cli security findings';
|
|
12
|
+
/** A pentest runs for hours, so its pending record goes stale later than a review's. */
|
|
13
|
+
export const PENTEST_PENDING_STALE_MS = 6 * 60 * 60 * 1000;
|
|
14
|
+
export const PENTEST_HINT = 'prism-cli security pentest --status';
|
|
15
|
+
/** Threats shown per repo; the rest are in the result file and `prism-cli security threats`. */
|
|
16
|
+
export const MAX_PANEL_THREATS = 5;
|
|
17
|
+
export const THREATS_HINT = 'prism-cli security threats';
|
|
18
|
+
const RANK = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
|
|
19
|
+
/**
|
|
20
|
+
* The latest job of a repo. Two files can hold the same job (code-review
|
|
21
|
+
* writes it with its scope, `security findings` again without); the one that
|
|
22
|
+
* knows its scope wins, then the newest write. A pending record is completed
|
|
23
|
+
* in place, but should a verdict for the same job exist elsewhere, it wins.
|
|
24
|
+
*/
|
|
25
|
+
export function latestSecurityReview(results) {
|
|
26
|
+
const byJob = new Map();
|
|
27
|
+
for (const r of results) {
|
|
28
|
+
if (r.kind !== 'security-review' || !r.security?.job_id)
|
|
29
|
+
continue;
|
|
30
|
+
const prev = byJob.get(r.security.job_id);
|
|
31
|
+
if (prev && (prev.status === 'pending') !== (r.status === 'pending')) {
|
|
32
|
+
if (prev.status === 'pending')
|
|
33
|
+
byJob.set(r.security.job_id, r);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const better = !prev || (prev.security.scope === 'unknown' && r.security.scope !== 'unknown')
|
|
37
|
+
|| (prev.security.scope === r.security.scope && r.finished_at > prev.finished_at)
|
|
38
|
+
|| (r.security.scope !== 'unknown' && r.finished_at > prev.finished_at);
|
|
39
|
+
if (better)
|
|
40
|
+
byJob.set(r.security.job_id, r);
|
|
41
|
+
}
|
|
42
|
+
const all = [...byJob.values()].sort((a, b) => b.started_at.localeCompare(a.started_at));
|
|
43
|
+
return all[0] ?? null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The latest pentest job of a repo. Like latestSecurityReview: newest write per
|
|
47
|
+
* job, but a completed record for a job wins over its pending record.
|
|
48
|
+
*/
|
|
49
|
+
export function latestPentest(results) {
|
|
50
|
+
const byJob = new Map();
|
|
51
|
+
for (const r of results) {
|
|
52
|
+
// A run that never started a job (the app never became healthy) has no job id but may still have a stack up.
|
|
53
|
+
const key = r.kind === 'security-pentest' ? (r.pentest?.job_id || r.pentest?.stack) : null;
|
|
54
|
+
if (!key)
|
|
55
|
+
continue;
|
|
56
|
+
const prev = byJob.get(key);
|
|
57
|
+
if (prev && (prev.status === 'pending') !== (r.status === 'pending')) {
|
|
58
|
+
if (prev.status === 'pending')
|
|
59
|
+
byJob.set(key, r);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!prev || r.finished_at > prev.finished_at)
|
|
63
|
+
byJob.set(key, r);
|
|
64
|
+
}
|
|
65
|
+
const all = [...byJob.values()].sort((a, b) => b.started_at.localeCompare(a.started_at));
|
|
66
|
+
return all[0] ?? null;
|
|
67
|
+
}
|
|
68
|
+
export function collectSecurity(repos, read = readResults, now = new Date()) {
|
|
69
|
+
const rows = [];
|
|
70
|
+
const pentests = [];
|
|
71
|
+
for (const repo of repos) {
|
|
72
|
+
const all = read(repo.path);
|
|
73
|
+
const r = latestSecurityReview(all);
|
|
74
|
+
if (r && r.security) {
|
|
75
|
+
const findings = [...(r.findings || [])]
|
|
76
|
+
.sort((a, b) => (RANK[b.risk_level || ''] ?? 0) - (RANK[a.risk_level || ''] ?? 0) || a.file.localeCompare(b.file))
|
|
77
|
+
.map((f) => ({ risk_level: f.risk_level || f.severity.toUpperCase(), title: f.title || f.message, file: f.file, line: f.line, cwe: f.cwe ?? null }));
|
|
78
|
+
rows.push({
|
|
79
|
+
name: repo.name, path: repo.path, current: repo.current,
|
|
80
|
+
job_id: r.security.job_id, job_status: r.security.job_status, status: r.status, scope: r.security.scope,
|
|
81
|
+
started_at: r.started_at, finished_at: r.finished_at, counts: r.security.counts || {},
|
|
82
|
+
findings: findings.slice(0, MAX_PANEL_FINDINGS), truncated: findings.length > MAX_PANEL_FINDINGS, reason: r.reason ?? null,
|
|
83
|
+
pending: r.status === 'pending',
|
|
84
|
+
stale: r.status === 'pending' && now.getTime() - Date.parse(r.started_at) > PENDING_STALE_MS,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const p = latestPentest(all);
|
|
88
|
+
if (p && p.pentest) {
|
|
89
|
+
const pf = [...(p.findings || [])]
|
|
90
|
+
.sort((a, b) => (RANK[b.risk_level || ''] ?? 0) - (RANK[a.risk_level || ''] ?? 0) || (a.title || '').localeCompare(b.title || ''))
|
|
91
|
+
.map((f) => ({ risk_level: f.risk_level || f.severity.toUpperCase(), title: f.title || f.message, confidence: f.confidence ?? null }));
|
|
92
|
+
pentests.push({
|
|
93
|
+
name: repo.name, path: repo.path, current: repo.current,
|
|
94
|
+
job_id: p.pentest.job_id, job_status: p.pentest.job_status, status: p.status,
|
|
95
|
+
target_host: p.pentest.target_host, started_at: p.started_at, finished_at: p.finished_at,
|
|
96
|
+
counts: p.pentest.counts || {}, findings: pf.slice(0, MAX_PANEL_FINDINGS), truncated: pf.length > MAX_PANEL_FINDINGS,
|
|
97
|
+
reason: p.reason ?? null, overview: p.pentest.overview ?? null,
|
|
98
|
+
teardown_failed: p.pentest.teardown_failed ?? null,
|
|
99
|
+
pending: p.status === 'pending',
|
|
100
|
+
stale: p.status === 'pending' && now.getTime() - Date.parse(p.started_at) > PENTEST_PENDING_STALE_MS,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
rows.sort((a, b) => Number(b.current) - Number(a.current) || b.started_at.localeCompare(a.started_at));
|
|
105
|
+
const tms = [];
|
|
106
|
+
for (const repo of repos) {
|
|
107
|
+
const r = latestThreatModel(read(repo.path));
|
|
108
|
+
if (r)
|
|
109
|
+
tms.push(threatModelRow(repo, r, now));
|
|
110
|
+
}
|
|
111
|
+
tms.sort((a, b) => Number(b.current) - Number(a.current) || b.started_at.localeCompare(a.started_at));
|
|
112
|
+
pentests.sort((a, b) => Number(b.current) - Number(a.current) || b.started_at.localeCompare(a.started_at));
|
|
113
|
+
return { repos: rows, pentests, hint: 'prism-cli security code-review', pending_hint: PENDING_HINT, pentest_hint: PENTEST_HINT, threat_models: tms, threats_hint: THREATS_HINT };
|
|
114
|
+
}
|
|
115
|
+
/** The newest threat model job of a repo; for one job, a verdict wins over a pending record. */
|
|
116
|
+
export function latestThreatModel(results) {
|
|
117
|
+
const byJob = new Map();
|
|
118
|
+
for (const r of results) {
|
|
119
|
+
if (r.kind !== 'security-threat-model' || !r.threat_model?.job_id)
|
|
120
|
+
continue;
|
|
121
|
+
const prev = byJob.get(r.threat_model.job_id);
|
|
122
|
+
const better = !prev || (prev.status === 'pending' && r.status !== 'pending')
|
|
123
|
+
|| ((prev.status === 'pending') === (r.status === 'pending') && r.finished_at > prev.finished_at);
|
|
124
|
+
if (better)
|
|
125
|
+
byJob.set(r.threat_model.job_id, r);
|
|
126
|
+
}
|
|
127
|
+
return [...byJob.values()].sort((a, b) => b.started_at.localeCompare(a.started_at))[0] ?? null;
|
|
128
|
+
}
|
|
129
|
+
function threatModelRow(repo, r, now) {
|
|
130
|
+
const t = r.threat_model;
|
|
131
|
+
const threats = [...(r.findings || [])]
|
|
132
|
+
.sort((a, b) => (RANK[b.risk_level || ''] ?? 0) - (RANK[a.risk_level || ''] ?? 0))
|
|
133
|
+
.map((f) => ({ severity: f.risk_level || f.severity.toUpperCase(), title: f.title || f.message, stride: f.stride || [] }));
|
|
134
|
+
return {
|
|
135
|
+
name: repo.name, path: repo.path, current: repo.current,
|
|
136
|
+
job_id: t.job_id, job_status: t.job_status, status: r.status, commit: r.commit,
|
|
137
|
+
started_at: r.started_at, finished_at: r.finished_at, counts: t.counts || {}, scope_docs: t.scope_docs || [],
|
|
138
|
+
overview: firstParagraph(t.system_overview),
|
|
139
|
+
threats: threats.slice(0, MAX_PANEL_THREATS), more: Math.max(0, threats.length - MAX_PANEL_THREATS), reason: r.reason ?? null,
|
|
140
|
+
pending: r.status === 'pending',
|
|
141
|
+
stale: r.status === 'pending' && now.getTime() - Date.parse(r.started_at) > PENDING_STALE_MS,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=security.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.js","sourceRoot":"","sources":["../../../src/dashboard/security.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loopback-only HTTP server for `prism-cli dashboard`.
|
|
3
|
+
*
|
|
4
|
+
* Access control, all enforced on every request:
|
|
5
|
+
* - bound to 127.0.0.1 only (the host is a constant, not an option);
|
|
6
|
+
* - a per-run 256-bit random token, accepted once from `/?token=` and
|
|
7
|
+
* exchanged for an HttpOnly, SameSite=Strict cookie, then required as that
|
|
8
|
+
* cookie on every request (compared in constant time);
|
|
9
|
+
* - the Host header must be 127.0.0.1:<port> or localhost:<port>, which
|
|
10
|
+
* defeats DNS rebinding (a rebound name arrives with its own Host);
|
|
11
|
+
* - GET only, read-only routes, no CORS headers, strict CSP.
|
|
12
|
+
*
|
|
13
|
+
* Why a minted token and not a platform auth mechanism: the server is
|
|
14
|
+
* reachable only from this machine, the audience is the developer running it,
|
|
15
|
+
* and the CLI ships as public sample code, so it uses the loopback-plus-token
|
|
16
|
+
* pattern native to local developer tools (Jupyter, VS Code server) rather
|
|
17
|
+
* than any hosted identity provider. It must never be bound beyond loopback.
|
|
18
|
+
*/
|
|
19
|
+
import { type IncomingMessage, type Server, type ServerResponse } from 'node:http';
|
|
20
|
+
import { type ScanReport } from './report.js';
|
|
21
|
+
export declare const BIND_HOST = "127.0.0.1";
|
|
22
|
+
export declare function newToken(): string;
|
|
23
|
+
export declare function cookieName(port: number): string;
|
|
24
|
+
export interface HandlerOptions {
|
|
25
|
+
token: string;
|
|
26
|
+
/** Read at request time: the real port is known only after listen(). */
|
|
27
|
+
getPort: () => number;
|
|
28
|
+
getPayload: () => Promise<unknown>;
|
|
29
|
+
/**
|
|
30
|
+
* Looks a report up by result id among the dashboard's repos. The id is
|
|
31
|
+
* matched against directory listings only; it is never turned into a path.
|
|
32
|
+
*/
|
|
33
|
+
getReport?: (id: string) => Promise<ScanReport | null>;
|
|
34
|
+
}
|
|
35
|
+
export declare function createHandler(opts: HandlerOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
36
|
+
export interface RunningServer {
|
|
37
|
+
server: Server;
|
|
38
|
+
port: number;
|
|
39
|
+
token: string;
|
|
40
|
+
url: string;
|
|
41
|
+
close: () => Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/** Starts the server on 127.0.0.1. `port` 0 (default) picks a free port. */
|
|
44
|
+
export declare function startServer(opts: {
|
|
45
|
+
port?: number;
|
|
46
|
+
token?: string;
|
|
47
|
+
getPayload: () => Promise<unknown>;
|
|
48
|
+
getReport?: (id: string) => Promise<ScanReport | null>;
|
|
49
|
+
}): Promise<RunningServer>;
|
|
50
|
+
/** Memoises the payload for `ttlMs` and shares one in-flight build between requests. */
|
|
51
|
+
export declare function cachedPayload(build: () => Promise<unknown>, ttlMs?: number): () => Promise<unknown>;
|
|
52
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/dashboard/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAgB,KAAK,eAAe,EAAE,KAAK,MAAM,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAGjG,OAAO,EAA0C,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAEtF,eAAO,MAAM,SAAS,cAAc,CAAC;AAErC,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI/C;AAqCD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,OAAO,EAAE,MAAM,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;CACxD;AAKD,wBAAgB,aAAa,CAAC,IAAI,EAAE,cAAc,IAClC,KAAK,eAAe,EAAE,KAAK,cAAc,KAAG,OAAO,CAAC,IAAI,CAAC,CAwDxE;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAAC,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAA;CAAE,GAAG,OAAO,CAAC,aAAa,CAAC,CAoBvL;AAED,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,SAAS,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,CAWnG"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loopback-only HTTP server for `prism-cli dashboard`.
|
|
3
|
+
*
|
|
4
|
+
* Access control, all enforced on every request:
|
|
5
|
+
* - bound to 127.0.0.1 only (the host is a constant, not an option);
|
|
6
|
+
* - a per-run 256-bit random token, accepted once from `/?token=` and
|
|
7
|
+
* exchanged for an HttpOnly, SameSite=Strict cookie, then required as that
|
|
8
|
+
* cookie on every request (compared in constant time);
|
|
9
|
+
* - the Host header must be 127.0.0.1:<port> or localhost:<port>, which
|
|
10
|
+
* defeats DNS rebinding (a rebound name arrives with its own Host);
|
|
11
|
+
* - GET only, read-only routes, no CORS headers, strict CSP.
|
|
12
|
+
*
|
|
13
|
+
* Why a minted token and not a platform auth mechanism: the server is
|
|
14
|
+
* reachable only from this machine, the audience is the developer running it,
|
|
15
|
+
* and the CLI ships as public sample code, so it uses the loopback-plus-token
|
|
16
|
+
* pattern native to local developer tools (Jupyter, VS Code server) rather
|
|
17
|
+
* than any hosted identity provider. It must never be bound beyond loopback.
|
|
18
|
+
*/
|
|
19
|
+
import { createServer } from 'node:http';
|
|
20
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
21
|
+
import { renderPage } from './page.js';
|
|
22
|
+
import { renderReportHtml, renderReportMarkdown } from './report.js';
|
|
23
|
+
export const BIND_HOST = '127.0.0.1';
|
|
24
|
+
export function newToken() {
|
|
25
|
+
return randomBytes(32).toString('base64url');
|
|
26
|
+
}
|
|
27
|
+
export function cookieName(port) {
|
|
28
|
+
// Cookies are scoped by host, not port: two dashboards on different ports
|
|
29
|
+
// would otherwise overwrite each other's cookie.
|
|
30
|
+
return `prism_dash_${port}`;
|
|
31
|
+
}
|
|
32
|
+
function safeEqual(a, b) {
|
|
33
|
+
const x = Buffer.from(a);
|
|
34
|
+
const y = Buffer.from(b);
|
|
35
|
+
return x.length === y.length && timingSafeEqual(x, y);
|
|
36
|
+
}
|
|
37
|
+
function readCookie(req, name) {
|
|
38
|
+
const raw = req.headers.cookie;
|
|
39
|
+
if (!raw)
|
|
40
|
+
return null;
|
|
41
|
+
for (const part of raw.split(';')) {
|
|
42
|
+
const i = part.indexOf('=');
|
|
43
|
+
if (i > 0 && part.slice(0, i).trim() === name)
|
|
44
|
+
return part.slice(i + 1).trim();
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function baseHeaders(nonce) {
|
|
49
|
+
const csp = nonce
|
|
50
|
+
? `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`
|
|
51
|
+
: "default-src 'none'; frame-ancestors 'none'";
|
|
52
|
+
return {
|
|
53
|
+
'Content-Security-Policy': csp,
|
|
54
|
+
'X-Content-Type-Options': 'nosniff',
|
|
55
|
+
'X-Frame-Options': 'DENY',
|
|
56
|
+
'Referrer-Policy': 'no-referrer',
|
|
57
|
+
'Cache-Control': 'no-store',
|
|
58
|
+
'Cross-Origin-Resource-Policy': 'same-origin',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function send(res, status, type, body, extra = {}, nonce) {
|
|
62
|
+
res.writeHead(status, { ...baseHeaders(nonce), 'Content-Type': type, ...extra });
|
|
63
|
+
res.end(body);
|
|
64
|
+
}
|
|
65
|
+
/** /report/<id> or /report/<id>.md; anything else (slashes, dots, %-escapes) does not match. */
|
|
66
|
+
const REPORT_ROUTE = /^\/report\/([A-Za-z0-9_-]{1,120})(\.md)?$/;
|
|
67
|
+
export function createHandler(opts) {
|
|
68
|
+
return async (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const port = opts.getPort();
|
|
71
|
+
const host = String(req.headers.host || '').toLowerCase();
|
|
72
|
+
if (host !== `${BIND_HOST}:${port}` && host !== `localhost:${port}`) {
|
|
73
|
+
return send(res, 403, 'text/plain; charset=utf-8', 'Forbidden: unexpected Host header\n');
|
|
74
|
+
}
|
|
75
|
+
if (req.method !== 'GET') {
|
|
76
|
+
return send(res, 405, 'text/plain; charset=utf-8', 'Method Not Allowed\n', { Allow: 'GET' });
|
|
77
|
+
}
|
|
78
|
+
const url = new URL(req.url || '/', `http://${BIND_HOST}:${port}`);
|
|
79
|
+
const name = cookieName(port);
|
|
80
|
+
const cookie = readCookie(req, name);
|
|
81
|
+
const cookieOk = cookie !== null && safeEqual(cookie, opts.token);
|
|
82
|
+
// First load: exchange the URL token for the cookie, then drop it from the URL.
|
|
83
|
+
const qToken = url.searchParams.get('token');
|
|
84
|
+
if (url.pathname === '/' && qToken !== null) {
|
|
85
|
+
if (!safeEqual(qToken, opts.token))
|
|
86
|
+
return send(res, 401, 'text/plain; charset=utf-8', 'Unauthorized\n');
|
|
87
|
+
return send(res, 303, 'text/plain; charset=utf-8', 'See Other\n', {
|
|
88
|
+
Location: '/',
|
|
89
|
+
'Set-Cookie': `${name}=${opts.token}; HttpOnly; SameSite=Strict; Path=/`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (!cookieOk) {
|
|
93
|
+
return send(res, 401, 'text/plain; charset=utf-8', 'Unauthorized: open the URL printed by prism-cli dashboard\n');
|
|
94
|
+
}
|
|
95
|
+
if (url.pathname === '/') {
|
|
96
|
+
const nonce = randomBytes(16).toString('base64');
|
|
97
|
+
return send(res, 200, 'text/html; charset=utf-8', renderPage(nonce), {}, nonce);
|
|
98
|
+
}
|
|
99
|
+
if (url.pathname === '/api/data') {
|
|
100
|
+
const payload = await opts.getPayload();
|
|
101
|
+
return send(res, 200, 'application/json; charset=utf-8', JSON.stringify(payload));
|
|
102
|
+
}
|
|
103
|
+
const m = REPORT_ROUTE.exec(url.pathname);
|
|
104
|
+
if (m && opts.getReport) {
|
|
105
|
+
const report = await opts.getReport(m[1]);
|
|
106
|
+
if (!report)
|
|
107
|
+
return send(res, 404, 'text/plain; charset=utf-8', 'Report not found\n');
|
|
108
|
+
if (m[2]) {
|
|
109
|
+
return send(res, 200, 'text/markdown; charset=utf-8', renderReportMarkdown(report), {
|
|
110
|
+
'Content-Disposition': `attachment; filename="codewatch-report-${m[1]}.md"`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const nonce = randomBytes(16).toString('base64');
|
|
114
|
+
return send(res, 200, 'text/html; charset=utf-8', renderReportHtml(report, { nonce, links: { back: '/', markdown: `/report/${m[1]}.md` } }), {}, nonce);
|
|
115
|
+
}
|
|
116
|
+
return send(res, 404, 'text/plain; charset=utf-8', 'Not Found\n');
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
if (!res.headersSent)
|
|
120
|
+
send(res, 500, 'text/plain; charset=utf-8', 'Internal Server Error\n');
|
|
121
|
+
else
|
|
122
|
+
res.end();
|
|
123
|
+
process.stderr.write(`prism dashboard: request failed: ${e?.message || e}\n`);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** Starts the server on 127.0.0.1. `port` 0 (default) picks a free port. */
|
|
128
|
+
export function startServer(opts) {
|
|
129
|
+
const token = opts.token || newToken();
|
|
130
|
+
let port = 0;
|
|
131
|
+
const server = createServer(createHandler({ token, getPort: () => port, getPayload: opts.getPayload, getReport: opts.getReport }));
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
server.once('error', reject);
|
|
134
|
+
server.listen(opts.port ?? 0, BIND_HOST, () => {
|
|
135
|
+
const addr = server.address();
|
|
136
|
+
if (!addr || typeof addr === 'string' || addr.address !== BIND_HOST) {
|
|
137
|
+
server.close();
|
|
138
|
+
return reject(new Error(`refusing to serve: bound to ${JSON.stringify(addr)}, expected ${BIND_HOST}`));
|
|
139
|
+
}
|
|
140
|
+
port = addr.port;
|
|
141
|
+
resolve({
|
|
142
|
+
server, port, token,
|
|
143
|
+
url: `http://${BIND_HOST}:${port}/?token=${token}`,
|
|
144
|
+
close: () => new Promise((r) => server.close(() => r())),
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/** Memoises the payload for `ttlMs` and shares one in-flight build between requests. */
|
|
150
|
+
export function cachedPayload(build, ttlMs = 30_000) {
|
|
151
|
+
let at = 0;
|
|
152
|
+
let pending = null;
|
|
153
|
+
let value;
|
|
154
|
+
return () => {
|
|
155
|
+
if (value !== undefined && Date.now() - at < ttlMs)
|
|
156
|
+
return Promise.resolve(value);
|
|
157
|
+
if (!pending) {
|
|
158
|
+
pending = build().then((v) => { value = v; at = Date.now(); return v; }).finally(() => { pending = null; });
|
|
159
|
+
}
|
|
160
|
+
return pending;
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=server.js.map
|