@wxxb789/anc 0.0.1
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 +295 -0
- package/astro.config.mjs +255 -0
- package/bin/anc.mjs +798 -0
- package/package.json +93 -0
- package/public/_headers +45 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +9 -0
- package/public/fonts/Temml.woff2 +0 -0
- package/scripts/build-snapshot.js +69 -0
- package/scripts/build-wasm.js +67 -0
- package/scripts/build-workspace.js +50 -0
- package/scripts/copy-snapshot.js +48 -0
- package/scripts/copy-wasm.js +57 -0
- package/scripts/emit-redirects.js +64 -0
- package/scripts/generate-corpus.js +421 -0
- package/scripts/init-repository.js +249 -0
- package/scripts/load-config.js +975 -0
- package/scripts/markdown-to-artifact.js +1286 -0
- package/scripts/preview-site.js +364 -0
- package/scripts/publish-set-review.js +157 -0
- package/scripts/resolve-links.js +640 -0
- package/scripts/run-pagefind.js +125 -0
- package/scripts/scan-residue.js +1016 -0
- package/scripts/scan-secrets.js +268 -0
- package/scripts/seed-gitignore.js +410 -0
- package/scripts/snapshot-rows.js +205 -0
- package/scripts/validate-content.js +95 -0
- package/scripts/vendor-provenance.js +307 -0
- package/scripts/verify-output-inventory.js +307 -0
- package/scripts/write-report.js +351 -0
- package/scripts/write-snapshot.js +136 -0
- package/src/components/CollectionExplorer.astro +149 -0
- package/src/components/FacetIndex.astro +45 -0
- package/src/components/LinkedNotes.astro +94 -0
- package/src/components/NoteGraph.astro +434 -0
- package/src/components/NoteList.astro +54 -0
- package/src/components/SiteMetadata.astro +143 -0
- package/src/components/TableOfContents.astro +69 -0
- package/src/components/TagBrowser.astro +70 -0
- package/src/layouts/Layout.astro +305 -0
- package/src/lib/artifact-source.js +72 -0
- package/src/lib/build-bindings.js +11 -0
- package/src/lib/collection-navigation.js +145 -0
- package/src/lib/content.js +92 -0
- package/src/lib/diagram-mode.js +18 -0
- package/src/lib/graph-layout.js +219 -0
- package/src/lib/graph-selection.js +84 -0
- package/src/lib/graph.js +42 -0
- package/src/lib/link-resolution.js +389 -0
- package/src/lib/markdown.js +1469 -0
- package/src/lib/math-mode.js +31 -0
- package/src/lib/math-output.js +131 -0
- package/src/lib/math.js +165 -0
- package/src/lib/mermaid-environment.js +475 -0
- package/src/lib/mermaid-render.js +769 -0
- package/src/lib/preview-model.js +96 -0
- package/src/lib/relations.js +242 -0
- package/src/lib/rendered-marker.js +55 -0
- package/src/lib/route-path.js +192 -0
- package/src/lib/routes.js +342 -0
- package/src/lib/schema.js +578 -0
- package/src/lib/site.js +578 -0
- package/src/lib/snapshot-contract.js +216 -0
- package/src/lib/snapshot-operations.js +147 -0
- package/src/lib/snapshot-queries.js +216 -0
- package/src/lib/snapshot-reader.js +210 -0
- package/src/lib/snapshot.js +233 -0
- package/src/lib/sqlite-warning.js +22 -0
- package/src/lib/sqlite.js +14 -0
- package/src/lib/tag-browser-model.js +18 -0
- package/src/lib/translations.js +533 -0
- package/src/lib/wasm-asset.js +47 -0
- package/src/lib/worker-protocol.js +93 -0
- package/src/pages/404.astro +35 -0
- package/src/pages/about.astro +100 -0
- package/src/pages/collections/[slug].astro +36 -0
- package/src/pages/collections/index.astro +33 -0
- package/src/pages/graph.astro +64 -0
- package/src/pages/index.astro +33 -0
- package/src/pages/notes/[slug].astro +509 -0
- package/src/pages/privacy.astro +121 -0
- package/src/pages/private.astro +59 -0
- package/src/pages/recent.astro +33 -0
- package/src/pages/robots.txt.js +15 -0
- package/src/pages/rss.xml.js +21 -0
- package/src/pages/sitemap.xml.js +20 -0
- package/src/pages/tags/[tag].astro +56 -0
- package/src/pages/tags/index.astro +37 -0
- package/src/scripts/code-copy.js +45 -0
- package/src/scripts/diagram-disabled.js +16 -0
- package/src/scripts/diagram.js +322 -0
- package/src/scripts/graph-client.js +222 -0
- package/src/scripts/graph-draw.js +166 -0
- package/src/scripts/link-preview.js +334 -0
- package/src/scripts/math-disabled.js +18 -0
- package/src/scripts/math.js +133 -0
- package/src/scripts/preferences.js +123 -0
- package/src/scripts/search-dialog.js +651 -0
- package/src/scripts/snapshot-client.js +157 -0
- package/src/scripts/snapshot-worker.js +341 -0
- package/src/scripts/tag-browser.js +153 -0
- package/src/scripts/theme-init.js +35 -0
- package/src/scripts/toc-active.js +38 -0
- package/src/styles/code.css +152 -0
- package/src/styles/diagram.css +149 -0
- package/src/styles/global.css +1739 -0
- package/src/styles/graph.css +317 -0
- package/src/styles/math.css +376 -0
- package/src/styles/tokens.css +171 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gitleaks over the exact bytes readers receive, including inflated gzip members.
|
|
3
|
+
*
|
|
4
|
+
* The external scanner owns the credential rule set and false-positive policy.
|
|
5
|
+
* This wrapper owns reproducibility, fail-closed process handling, redaction, and
|
|
6
|
+
* the public-count/private-path split used by every other publication gate.
|
|
7
|
+
*/
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync, } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { gunzipSync } from 'node:zlib';
|
|
14
|
+
import { SNAPSHOT_DIRECTORY, SNAPSHOT_FILE_PATTERN } from "../src/lib/snapshot.js";
|
|
15
|
+
import { BuildFailure } from "./write-report.js";
|
|
16
|
+
import { databaseText, isGzip, isSqlite } from "./snapshot-rows.js";
|
|
17
|
+
export const GITLEAKS_VERSION = '8.30.1';
|
|
18
|
+
const DIST = fileURLToPath(new URL('../dist', import.meta.url));
|
|
19
|
+
function processText(value) {
|
|
20
|
+
return value ?? '';
|
|
21
|
+
}
|
|
22
|
+
function scanProcessDetail(result, findings = []) {
|
|
23
|
+
return JSON.stringify({
|
|
24
|
+
status: result.status,
|
|
25
|
+
error: result.error?.message,
|
|
26
|
+
stdoutBytes: Buffer.byteLength(processText(result.stdout), 'utf8'),
|
|
27
|
+
stderrBytes: Buffer.byteLength(processText(result.stderr), 'utf8'),
|
|
28
|
+
findings,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function filesUnder(root) {
|
|
32
|
+
const files = [];
|
|
33
|
+
const pending = [root];
|
|
34
|
+
while (pending.length > 0) {
|
|
35
|
+
const directory = pending.pop();
|
|
36
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
37
|
+
const path = join(directory, entry.name);
|
|
38
|
+
if (entry.isDirectory())
|
|
39
|
+
pending.push(path);
|
|
40
|
+
else if (entry.isFile())
|
|
41
|
+
files.push(path);
|
|
42
|
+
else {
|
|
43
|
+
throw new BuildFailure('secret-scan-input-invalid', 'secret scan found a non-file output member', relative(root, path));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return files.sort();
|
|
48
|
+
}
|
|
49
|
+
function projection(source, target) {
|
|
50
|
+
const files = filesUnder(source);
|
|
51
|
+
if (files.length === 0) {
|
|
52
|
+
throw new BuildFailure('secret-scan-input-empty', 'secret scan refused an empty output', source);
|
|
53
|
+
}
|
|
54
|
+
for (const path of files) {
|
|
55
|
+
const relativePath = relative(source, path);
|
|
56
|
+
const bytes = readFileSync(path);
|
|
57
|
+
const gzip = isGzip(bytes);
|
|
58
|
+
const destination = join(target, relativePath);
|
|
59
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
60
|
+
// Raw-byte coverage is unchanged: the member is copied verbatim, so a value
|
|
61
|
+
// that survives only in a free page is still scanned as bytes.
|
|
62
|
+
copyFileSync(path, destination);
|
|
63
|
+
let payload = bytes;
|
|
64
|
+
if (gzip) {
|
|
65
|
+
try {
|
|
66
|
+
payload = gunzipSync(bytes);
|
|
67
|
+
writeFileSync(destination + '.inflated', payload);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
throw new BuildFailure('secret-scan-input-unreadable', 'secret scan could not inflate one output member', relativePath + ': ' + (error instanceof Error ? error.stack ?? error.message : String(error)));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// A database is recognised by its magic, on the inflated payload, so gzip and
|
|
74
|
+
// any filename are the same case. The scanner copies raw bytes, so a row a
|
|
75
|
+
// `SELECT` cannot reach (a deleted payload in a free page) is still scanned;
|
|
76
|
+
// the reconstructed rows are a separate member, so a value no byte read can
|
|
77
|
+
// spell — a BLOB stored as gzip, or one split across an overflow-page pointer
|
|
78
|
+
// — reaches Gitleaks as text.
|
|
79
|
+
if (isSqlite(payload)) {
|
|
80
|
+
let read;
|
|
81
|
+
try {
|
|
82
|
+
read = databaseText(payload);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
throw new BuildFailure('secret-scan-input-unreadable', 'secret scan could not reconstruct one database member', relativePath + ': ' + (error instanceof Error ? error.message : String(error)));
|
|
86
|
+
}
|
|
87
|
+
if (read.unreadable.length > 0) {
|
|
88
|
+
throw new BuildFailure('secret-scan-input-unreadable', 'secret scan could not read every table in one database member', relativePath + ': ' + read.unreadable.join(', '));
|
|
89
|
+
}
|
|
90
|
+
writeFileSync(destination + '.rows', read.values.length === 0 ? '' : `${read.values.join('\n')}\n`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return files.length;
|
|
94
|
+
}
|
|
95
|
+
function cleanString(value) {
|
|
96
|
+
return typeof value === 'string' ? value : undefined;
|
|
97
|
+
}
|
|
98
|
+
function cleanNumber(value) {
|
|
99
|
+
return typeof value === 'number' && Number.isSafeInteger(value) ? value : undefined;
|
|
100
|
+
}
|
|
101
|
+
function projectedPath(file, projectionRoot) {
|
|
102
|
+
if (file === undefined)
|
|
103
|
+
return undefined;
|
|
104
|
+
const absolute = isAbsolute(file) ? resolve(file) : resolve(process.cwd(), file);
|
|
105
|
+
const path = relative(projectionRoot, absolute);
|
|
106
|
+
if (path === '..' || path.startsWith('..' + sep) || isAbsolute(path))
|
|
107
|
+
return '<outside projection>';
|
|
108
|
+
return path.split(sep).join('/');
|
|
109
|
+
}
|
|
110
|
+
/** A sidecar this scan adds to a projected member. */
|
|
111
|
+
const PROJECTED_SIDECAR = /\.(inflated|rows)$/;
|
|
112
|
+
/**
|
|
113
|
+
* The public half of one projected path.
|
|
114
|
+
*
|
|
115
|
+
* The snapshot's name is `site.<sha256>.sqlite` and the digest tracks the corpus,
|
|
116
|
+
* so it may not reach a stream. It is replaced by one stable literal, keeping the
|
|
117
|
+
* representation as a suffix because the operator's next step differs: raw bytes
|
|
118
|
+
* can be a deleted payload in a free page; rows are a live stored value.
|
|
119
|
+
*/
|
|
120
|
+
function publicFindingFile(projected) {
|
|
121
|
+
if (projected === undefined || projected === '<outside projection>')
|
|
122
|
+
return projected;
|
|
123
|
+
const segments = projected.split('/');
|
|
124
|
+
if (segments.length !== 2 || segments[0] !== SNAPSHOT_DIRECTORY)
|
|
125
|
+
return projected;
|
|
126
|
+
const name = segments[1];
|
|
127
|
+
if (!SNAPSHOT_FILE_PATTERN.test(name.replace(PROJECTED_SIDECAR, '')))
|
|
128
|
+
return projected;
|
|
129
|
+
const which = name.endsWith('.rows') ? ', reconstructed rows' : name.endsWith('.inflated') ? ', inflated' : '';
|
|
130
|
+
return `the site snapshot (${SNAPSHOT_DIRECTORY}/)${which}`;
|
|
131
|
+
}
|
|
132
|
+
function sanitize(findings, projectionRoot) {
|
|
133
|
+
if (!Array.isArray(findings)) {
|
|
134
|
+
throw new BuildFailure('secret-scanner-failed', 'secret scan failed to produce a valid report', 'Gitleaks JSON report is not an array');
|
|
135
|
+
}
|
|
136
|
+
return findings.map((value, index) => {
|
|
137
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
138
|
+
throw new BuildFailure('secret-scanner-failed', 'secret scan failed to produce a valid report', 'Gitleaks finding ' + index + ' is not an object');
|
|
139
|
+
}
|
|
140
|
+
const finding = value;
|
|
141
|
+
if (finding.Secret !== 'REDACTED' ||
|
|
142
|
+
typeof finding.Match !== 'string' ||
|
|
143
|
+
!finding.Match.includes('REDACTED')) {
|
|
144
|
+
throw new BuildFailure('secret-scanner-failed', 'secret scan report was not fully redacted', 'Gitleaks finding ' + index + ' did not redact Match and Secret');
|
|
145
|
+
}
|
|
146
|
+
const projected = projectedPath(cleanString(finding.File), projectionRoot);
|
|
147
|
+
return {
|
|
148
|
+
rule: cleanString(finding.RuleID),
|
|
149
|
+
description: cleanString(finding.Description),
|
|
150
|
+
file: publicFindingFile(projected),
|
|
151
|
+
startLine: cleanNumber(finding.StartLine),
|
|
152
|
+
endLine: cleanNumber(finding.EndLine),
|
|
153
|
+
startColumn: cleanNumber(finding.StartColumn),
|
|
154
|
+
endColumn: cleanNumber(finding.EndColumn),
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function scannerEnvironment() {
|
|
159
|
+
const environment = { ...process.env };
|
|
160
|
+
delete environment['GITLEAKS_CONFIG'];
|
|
161
|
+
delete environment['GITLEAKS_CONFIG_TOML'];
|
|
162
|
+
return environment;
|
|
163
|
+
}
|
|
164
|
+
/** Run the pinned scanner without forwarding its path-bearing process streams. */
|
|
165
|
+
export function scanSecrets(root, options = {}) {
|
|
166
|
+
const executable = options.executable ?? 'gitleaks';
|
|
167
|
+
const expectedVersion = options.expectedVersion ?? GITLEAKS_VERSION;
|
|
168
|
+
const environment = scannerEnvironment();
|
|
169
|
+
const version = spawnSync(executable, ['version'], {
|
|
170
|
+
encoding: 'utf8',
|
|
171
|
+
env: environment,
|
|
172
|
+
timeout: 30_000,
|
|
173
|
+
windowsHide: true,
|
|
174
|
+
});
|
|
175
|
+
if (version.status !== 0 ||
|
|
176
|
+
processText(version.stdout).trim() !== expectedVersion ||
|
|
177
|
+
processText(version.stderr).trim() !== '') {
|
|
178
|
+
throw new BuildFailure('secret-scanner-unavailable', 'secret scan requires Gitleaks ' + expectedVersion, scanProcessDetail(version));
|
|
179
|
+
}
|
|
180
|
+
const temporary = mkdtempSync(join(tmpdir(), 'anc-secret-scan-'));
|
|
181
|
+
const scanRoot = join(temporary, 'artifact');
|
|
182
|
+
const report = join(temporary, 'report.json');
|
|
183
|
+
const ignore = join(temporary, 'empty.gitleaksignore');
|
|
184
|
+
const config = join(temporary, 'gitleaks.toml');
|
|
185
|
+
const run = () => {
|
|
186
|
+
mkdirSync(scanRoot);
|
|
187
|
+
writeFileSync(ignore, '', 'utf8');
|
|
188
|
+
writeFileSync(config, '[extend]\nuseDefault = true\n', 'utf8');
|
|
189
|
+
const scanned = projection(root, scanRoot);
|
|
190
|
+
const result = spawnSync(executable, [
|
|
191
|
+
'dir',
|
|
192
|
+
'--no-banner',
|
|
193
|
+
'--no-color',
|
|
194
|
+
'--redact=100',
|
|
195
|
+
'--log-level', 'error',
|
|
196
|
+
'--ignore-gitleaks-allow',
|
|
197
|
+
'--gitleaks-ignore-path', ignore,
|
|
198
|
+
'--config', config,
|
|
199
|
+
// Gzip is projected explicitly above. One encoding layer still catches
|
|
200
|
+
// wrapped credentials; depth five spent ~80 seconds on Mermaid bundles.
|
|
201
|
+
'--max-archive-depth', '0',
|
|
202
|
+
'--max-decode-depth', '1',
|
|
203
|
+
// Zero disables size-based skipping: a large output member must be read.
|
|
204
|
+
'--max-target-megabytes', '0',
|
|
205
|
+
'--timeout', '120',
|
|
206
|
+
'--exit-code', '7',
|
|
207
|
+
'--report-format', 'json',
|
|
208
|
+
'--report-path', report,
|
|
209
|
+
scanRoot,
|
|
210
|
+
], { encoding: 'utf8', env: environment, timeout: 130_000, windowsHide: true });
|
|
211
|
+
let rawReport;
|
|
212
|
+
try {
|
|
213
|
+
rawReport = JSON.parse(readFileSync(report, 'utf8'));
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
throw new BuildFailure('secret-scanner-failed', 'secret scan failed to produce a readable report', scanProcessDetail(result) + '\n' + String(error));
|
|
217
|
+
}
|
|
218
|
+
const findings = sanitize(rawReport, scanRoot);
|
|
219
|
+
// At error log level, any stderr means Gitleaks could not read something.
|
|
220
|
+
if (processText(result.stderr).trim() !== '' || (result.status !== 0 && result.status !== 7)) {
|
|
221
|
+
throw new BuildFailure('secret-scanner-failed', 'secret scan did not inspect the complete output', scanProcessDetail(result, findings));
|
|
222
|
+
}
|
|
223
|
+
const hasFindings = findings.length > 0;
|
|
224
|
+
if ((result.status === 7) !== hasFindings) {
|
|
225
|
+
throw new BuildFailure('secret-scanner-failed', 'secret scan status disagreed with its redacted report', scanProcessDetail(result, findings));
|
|
226
|
+
}
|
|
227
|
+
if (hasFindings) {
|
|
228
|
+
throw new BuildFailure('secret-scan-findings', 'secret scan found ' + findings.length + ' finding' + (findings.length === 1 ? '' : 's'), JSON.stringify({ findings }));
|
|
229
|
+
}
|
|
230
|
+
return scanned;
|
|
231
|
+
};
|
|
232
|
+
let outcome = 0;
|
|
233
|
+
let failure;
|
|
234
|
+
try {
|
|
235
|
+
outcome = run();
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
failure = error;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
rmSync(temporary, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
|
|
242
|
+
}
|
|
243
|
+
catch (cleanupError) {
|
|
244
|
+
const detail = cleanupError instanceof Error ? cleanupError.stack ?? cleanupError.message : String(cleanupError);
|
|
245
|
+
failure = failure instanceof BuildFailure
|
|
246
|
+
? new BuildFailure(failure.code, failure.message, failure.detail + '\n' + detail)
|
|
247
|
+
: new BuildFailure('secret-scan-cleanup-failed', 'secret scan could not remove its temporary projection', detail);
|
|
248
|
+
}
|
|
249
|
+
if (failure instanceof BuildFailure)
|
|
250
|
+
throw failure;
|
|
251
|
+
if (failure !== undefined) {
|
|
252
|
+
throw new BuildFailure('secret-scanner-failed', 'secret scan failed before inspecting the complete output', failure instanceof Error ? failure.stack ?? failure.message : String(failure));
|
|
253
|
+
}
|
|
254
|
+
return outcome;
|
|
255
|
+
}
|
|
256
|
+
/** Own-repository command; the packaged binary imports scanSecrets directly. */
|
|
257
|
+
export function runSecretScanCommand() {
|
|
258
|
+
try {
|
|
259
|
+
console.log('secret scan ok: ' + scanSecrets(DIST) + ' files, 0 findings');
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
console.error(error instanceof BuildFailure ? error.message : 'secret scan failed');
|
|
264
|
+
return 1;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (process.argv[1] === fileURLToPath(import.meta.url))
|
|
268
|
+
process.exit(runSecretScanCommand());
|