@mnstry/atelier 0.2.0-alpha.4 → 0.2.0-alpha.6
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/CHANGELOG.md +77 -0
- package/README.md +61 -18
- package/contracts/atelier-repository-observation.v1.schema.json +163 -0
- package/contracts/public-api-baseline.json +57 -0
- package/docs/assurance-controls.md +41 -0
- package/docs/atelier-runtime.md +28 -2
- package/docs/atelier-sync.md +171 -0
- package/docs/blocks/claims.md +23 -12
- package/docs/blocks/will-not-do.md +9 -3
- package/docs/design.md +12 -6
- package/docs/install.md +26 -4
- package/docs/knowledge-graph.md +8 -4
- package/docs/local-services.md +101 -0
- package/docs/release-engineering.md +86 -12
- package/docs/repo-boundary-guard.md +12 -2
- package/docs/upgrade.md +40 -2
- package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
- package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
- package/fixtures/projects/sample-workspace/content/source.html.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/data.json.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/logo.png.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/metrics.csv.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/pipeline.yaml.kg.json +4 -1
- package/package.json +16 -5
- package/skills/claude/atelier-local-service/SKILL.md +47 -0
- package/skills/claude/atelier-public-boundary/SKILL.md +31 -0
- package/skills/codex/atelier-local-service/SKILL.md +47 -0
- package/skills/codex/atelier-public-boundary/SKILL.md +31 -0
- package/src/boundary/content-rules.mjs +283 -20
- package/src/boundary/policy.mjs +162 -72
- package/src/cli/execute-command.mjs +36 -0
- package/src/cli/run.mjs +35 -7
- package/src/collaboration/event-ledger.mjs +365 -0
- package/src/collaboration/index.mjs +17 -0
- package/src/collaboration/proposals.mjs +265 -65
- package/src/commands/attestation.mjs +20 -6
- package/src/commands/disclosure.mjs +133 -0
- package/src/commands/distribution.mjs +2 -1
- package/src/commands/extension-pack.mjs +2 -1
- package/src/commands/init.mjs +2 -1
- package/src/commands/server.mjs +1 -4
- package/src/commands/sync.mjs +100 -0
- package/src/contracts/corpus.mjs +6 -0
- package/src/disclosure/content-scan.mjs +193 -0
- package/src/egress/check.mjs +7 -38
- package/src/egress/forbidden-egress.mjs +32 -18
- package/src/graph/graph.mjs +112 -314
- package/src/graph/knowledge-graph.mjs +94 -18
- package/src/harness/context-client.mjs +9 -1
- package/src/index.mjs +41 -0
- package/src/project/config.mjs +89 -28
- package/src/project/file-class.mjs +14 -0
- package/src/project/package-root.mjs +10 -0
- package/src/project/path-match.mjs +38 -15
- package/src/project/private-state.mjs +110 -0
- package/src/runtime/git-adapter.mjs +189 -0
- package/src/runtime/local-state.mjs +439 -0
- package/src/runtime/repository-observation.mjs +491 -0
- package/src/runtime/supervisor.mjs +788 -0
- package/src/server/local-sidecar.mjs +81 -59
- package/src/server/security.mjs +89 -4
- package/src/server/server.mjs +3 -2
- package/src/support/feedback-report.mjs +4 -3
- package/src/upgrade/upgrade.mjs +2 -1
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
2
4
|
import { matchesPathPattern, normalizeRelPath } from '../project/path-match.mjs'
|
|
5
|
+
import { sanitizedGitEnvironment } from '../runtime/git-adapter.mjs'
|
|
3
6
|
|
|
4
7
|
export const EMPTY_TREE = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
|
|
5
8
|
export const ZERO_SHA_RE = /^0{40,}$/
|
|
6
9
|
|
|
7
10
|
export const RULE_KINDS = Object.freeze(['content', 'path'])
|
|
8
11
|
export const RULE_SEVERITIES = Object.freeze(['error', 'warning'])
|
|
12
|
+
export const DIFF_RESULT_MAX_BYTES = 16 * 1024 * 1024
|
|
13
|
+
export const CHECK_AGGREGATE_MAX_BYTES = 64 * 1024 * 1024
|
|
14
|
+
export const BINARY_FILE_MAX_BYTES = 8 * 1024 * 1024
|
|
15
|
+
export const BINARY_AGGREGATE_MAX_BYTES = 32 * 1024 * 1024
|
|
9
16
|
|
|
10
17
|
// A blanket exception is not an exception, it is switching the rule off. The
|
|
11
18
|
// contract requires a rule, a repo, real paths, and a reason a reviewer can act on.
|
|
@@ -33,7 +40,8 @@ export const DEFAULT_CONTENT_RULES = Object.freeze(
|
|
|
33
40
|
id: 'private-financial-filename',
|
|
34
41
|
kind: 'path',
|
|
35
42
|
severity: 'error',
|
|
36
|
-
pattern:
|
|
43
|
+
pattern:
|
|
44
|
+
'((^|/)\\.env(?:$|[.-])|invoice|payroll|(^|[-_/])salar(y|ies)|bank[-_]?statement|tax[-_]?return|credential|(^|[-_/])password([-_.]|$)|(^|[-_/])secret([-_.]|$)|id_rsa)',
|
|
37
45
|
description: 'Private or financial material does not belong in a source repo.',
|
|
38
46
|
},
|
|
39
47
|
].map(Object.freeze),
|
|
@@ -45,6 +53,7 @@ const trimmed = (value) => (typeof value === 'string' ? value.trim() : '')
|
|
|
45
53
|
export function validateContentRules(rules, { label = 'contentRules' } = {}) {
|
|
46
54
|
const errors = []
|
|
47
55
|
if (!Array.isArray(rules)) return [`${label} must be an array`]
|
|
56
|
+
if (rules.length === 0) errors.push(`${label} must contain at least one rule; omit the field to use the defaults`)
|
|
48
57
|
const seen = new Set()
|
|
49
58
|
for (const [index, rule] of rules.entries()) {
|
|
50
59
|
const at = `${label}[${index}]`
|
|
@@ -112,7 +121,8 @@ export function validateContentRuleExceptions(exceptions, rules = DEFAULT_CONTEN
|
|
|
112
121
|
|
|
113
122
|
export function resolveContentRules(policy) {
|
|
114
123
|
const declared = Array.isArray(policy?.contentRules) ? policy.contentRules : null
|
|
115
|
-
|
|
124
|
+
if (!declared?.length || validateContentRules(declared).length > 0) return DEFAULT_CONTENT_RULES
|
|
125
|
+
return declared
|
|
116
126
|
}
|
|
117
127
|
|
|
118
128
|
export function findException({ exceptions = [], rule, repo, path: filePath }) {
|
|
@@ -209,36 +219,237 @@ export function parseAddedContent(diff) {
|
|
|
209
219
|
|
|
210
220
|
/** Ref updates arrive on the pre-push hook's stdin as `<localRef> <localSha> <remoteRef> <remoteSha>`. */
|
|
211
221
|
export function parsePushRefUpdates(text) {
|
|
222
|
+
return parsePushRefInput(text).updates
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function parsePushRefInput(text) {
|
|
212
226
|
const updates = []
|
|
213
|
-
|
|
227
|
+
const issues = []
|
|
228
|
+
const input = String(text ?? '')
|
|
229
|
+
if (!input.trim()) return { ok: true, kind: 'empty', updates, issues }
|
|
230
|
+
for (const [index, raw] of input.split(/\r?\n/).entries()) {
|
|
231
|
+
if (!raw.trim()) continue
|
|
214
232
|
const parts = raw.trim().split(/\s+/).filter(Boolean)
|
|
215
|
-
if (parts.length
|
|
233
|
+
if (parts.length !== 4) {
|
|
234
|
+
issues.push(`line ${index + 1}: expected four pre-push fields`)
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
216
237
|
const [localRef, localSha, remoteRef, remoteSha] = parts
|
|
238
|
+
if (!/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/i.test(localSha) || !/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/i.test(remoteSha)) {
|
|
239
|
+
issues.push(`line ${index + 1}: ref update contains an invalid object id`)
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
if (!localRef || !remoteRef) {
|
|
243
|
+
issues.push(`line ${index + 1}: ref names are required`)
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
217
246
|
if (ZERO_SHA_RE.test(localSha)) continue // branch deletion pushes no content
|
|
218
247
|
updates.push({ localRef, localSha, remoteRef, remoteSha })
|
|
219
248
|
}
|
|
220
|
-
return
|
|
249
|
+
return issues.length > 0
|
|
250
|
+
? { ok: false, kind: 'invalid', updates: [], issues }
|
|
251
|
+
: { ok: true, kind: updates.length > 0 ? 'valid' : 'empty', updates, issues: [] }
|
|
221
252
|
}
|
|
222
253
|
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
|
|
254
|
+
export function gitOutputResult(repoRoot, args, {
|
|
255
|
+
encoding = 'utf8',
|
|
256
|
+
gitExecutable = 'git',
|
|
257
|
+
maxBuffer = DIFF_RESULT_MAX_BYTES,
|
|
258
|
+
runner = spawnSync,
|
|
259
|
+
} = {}) {
|
|
260
|
+
let result
|
|
261
|
+
try {
|
|
262
|
+
result = runner(gitExecutable, ['-C', repoRoot, ...args], { encoding, maxBuffer, env: sanitizedGitEnvironment() })
|
|
263
|
+
} catch (error) {
|
|
264
|
+
return { ok: false, code: 'git-command-failed', error: error instanceof Error ? error.message : String(error), stdout: null }
|
|
265
|
+
}
|
|
266
|
+
if (result?.error?.code === 'ENOBUFS') {
|
|
267
|
+
return { ok: false, code: 'git-output-limit-exceeded', error: `Git ${args[0] ?? 'command'} exceeded the ${maxBuffer}-byte evidence limit`, stdout: null }
|
|
268
|
+
}
|
|
269
|
+
if (result?.status !== 0) {
|
|
270
|
+
return { ok: false, code: 'git-command-failed', error: `Git ${args[0] ?? 'command'} failed with status ${result?.status ?? 'unknown'}`, stdout: null }
|
|
271
|
+
}
|
|
272
|
+
const stdout = result.stdout ?? (encoding === 'buffer' ? Buffer.alloc(0) : '')
|
|
273
|
+
const bytes = Buffer.isBuffer(stdout) ? stdout.length : Buffer.byteLength(stdout)
|
|
274
|
+
if (bytes > maxBuffer) {
|
|
275
|
+
return { ok: false, code: 'git-output-limit-exceeded', error: `Git ${args[0] ?? 'command'} exceeded the ${maxBuffer}-byte evidence limit`, stdout: null }
|
|
276
|
+
}
|
|
277
|
+
return { ok: true, stdout, bytes }
|
|
226
278
|
}
|
|
227
279
|
|
|
228
280
|
export function pushRangeBase(repoRoot, remoteSha) {
|
|
229
281
|
// A new branch has no remote counterpart, so everything it carries is new:
|
|
230
282
|
// diff against the empty tree rather than silently scanning nothing.
|
|
231
283
|
if (!remoteSha || ZERO_SHA_RE.test(remoteSha)) return EMPTY_TREE
|
|
232
|
-
|
|
284
|
+
const result = spawnSync('git', ['-C', repoRoot, 'rev-parse', '--verify', '--quiet', `${remoteSha}^{commit}`], {
|
|
285
|
+
encoding: 'utf8',
|
|
286
|
+
maxBuffer: 1024 * 1024,
|
|
287
|
+
})
|
|
288
|
+
return result.status === 0 ? remoteSha : EMPTY_TREE
|
|
233
289
|
}
|
|
234
290
|
|
|
235
|
-
export function diffForPushRange(repoRoot, { localSha, remoteSha }) {
|
|
291
|
+
export function diffForPushRange(repoRoot, { localSha, remoteSha }, options = {}) {
|
|
236
292
|
const base = pushRangeBase(repoRoot, remoteSha)
|
|
237
|
-
|
|
293
|
+
const result = gitOutputResult(repoRoot, ['diff', '--unified=0', base, localSha], options)
|
|
294
|
+
return result.ok ? { ...result, base, diff: result.stdout } : { ...result, base, diff: null }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function stagedDiff(repoRoot, options = {}) {
|
|
298
|
+
const result = gitOutputResult(repoRoot, ['diff', '--cached', '--unified=0'], options)
|
|
299
|
+
return result.ok ? { ...result, diff: result.stdout } : { ...result, diff: null }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function incompleteDiagnostic({ repo, path: filePath = null, message, details = {} }) {
|
|
303
|
+
return {
|
|
304
|
+
severity: 'error',
|
|
305
|
+
code: 'content-scan-incomplete',
|
|
306
|
+
repo,
|
|
307
|
+
path: filePath,
|
|
308
|
+
line: null,
|
|
309
|
+
message: `${repo ?? 'repository'}${filePath ? `/${filePath}` : ''}: ${message}`,
|
|
310
|
+
details,
|
|
311
|
+
}
|
|
238
312
|
}
|
|
239
313
|
|
|
240
|
-
|
|
241
|
-
|
|
314
|
+
function binaryPathsFromDiff(diff) {
|
|
315
|
+
const paths = new Set()
|
|
316
|
+
let current = null
|
|
317
|
+
for (const line of String(diff ?? '').split('\n')) {
|
|
318
|
+
if (line.startsWith('diff --git ')) {
|
|
319
|
+
const header = line.match(/^diff --git a\/(.*) b\/(.*)$/)
|
|
320
|
+
current = header ? normalizeRelPath(header[2]) : null
|
|
321
|
+
continue
|
|
322
|
+
}
|
|
323
|
+
if (current && (line.startsWith('Binary files ') || line === 'GIT binary patch')) paths.add(current)
|
|
324
|
+
}
|
|
325
|
+
return [...paths]
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function isBinaryBuffer(buffer) {
|
|
329
|
+
if (buffer.subarray(0, Math.min(buffer.length, 8192)).includes(0)) return true
|
|
330
|
+
try {
|
|
331
|
+
new TextDecoder('utf-8', { fatal: true }).decode(buffer)
|
|
332
|
+
return false
|
|
333
|
+
} catch {
|
|
334
|
+
return true
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function scanBinaryBuffer({ buffer, filePath, rules, exceptions, repo }) {
|
|
339
|
+
if (!isBinaryBuffer(buffer)) return []
|
|
340
|
+
const text = buffer.toString('latin1')
|
|
341
|
+
const findings = []
|
|
342
|
+
for (const rule of rules) {
|
|
343
|
+
if ((rule.kind ?? 'content') !== 'content' || !rulePathMatches(rule, filePath)) continue
|
|
344
|
+
if (!new RegExp(rule.pattern).test(text)) continue
|
|
345
|
+
if (findException({ exceptions, rule: rule.id, repo, path: filePath })) continue
|
|
346
|
+
findings.push({
|
|
347
|
+
severity: rule.severity ?? 'error',
|
|
348
|
+
code: 'content-rule-violation',
|
|
349
|
+
rule: rule.id,
|
|
350
|
+
repo,
|
|
351
|
+
path: filePath,
|
|
352
|
+
line: null,
|
|
353
|
+
binary: true,
|
|
354
|
+
message: `${repo ?? ''}/${filePath}: ${rule.description ?? rule.id} (binary content)`.replace(/^\//, ''),
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
return findings
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function readBinaryChanges({ repoRoot, revision, diff, rules, exceptions, repo, gitExecutable, gitRunner }) {
|
|
361
|
+
const findings = []
|
|
362
|
+
const diagnostics = []
|
|
363
|
+
let totalBytes = 0
|
|
364
|
+
for (const filePath of binaryPathsFromDiff(diff)) {
|
|
365
|
+
const spec = revision === ':' ? `:${filePath}` : `${revision}:${filePath}`
|
|
366
|
+
const result = gitOutputResult(repoRoot, ['show', spec], {
|
|
367
|
+
encoding: 'buffer',
|
|
368
|
+
gitExecutable,
|
|
369
|
+
maxBuffer: BINARY_FILE_MAX_BYTES + 1,
|
|
370
|
+
runner: gitRunner,
|
|
371
|
+
})
|
|
372
|
+
if (!result.ok) {
|
|
373
|
+
diagnostics.push(incompleteDiagnostic({
|
|
374
|
+
repo,
|
|
375
|
+
path: filePath,
|
|
376
|
+
message: `binary content could not be read within the ${BINARY_FILE_MAX_BYTES}-byte per-file limit`,
|
|
377
|
+
details: { reason: result.code, limitBytes: BINARY_FILE_MAX_BYTES },
|
|
378
|
+
}))
|
|
379
|
+
continue
|
|
380
|
+
}
|
|
381
|
+
if (result.bytes > BINARY_FILE_MAX_BYTES) {
|
|
382
|
+
diagnostics.push(incompleteDiagnostic({
|
|
383
|
+
repo,
|
|
384
|
+
path: filePath,
|
|
385
|
+
message: `binary content exceeds the ${BINARY_FILE_MAX_BYTES}-byte per-file limit`,
|
|
386
|
+
details: { limitBytes: BINARY_FILE_MAX_BYTES, observedBytes: result.bytes },
|
|
387
|
+
}))
|
|
388
|
+
continue
|
|
389
|
+
}
|
|
390
|
+
totalBytes += result.bytes
|
|
391
|
+
if (totalBytes > BINARY_AGGREGATE_MAX_BYTES) {
|
|
392
|
+
diagnostics.push(incompleteDiagnostic({
|
|
393
|
+
repo,
|
|
394
|
+
path: filePath,
|
|
395
|
+
message: `binary content exceeds the ${BINARY_AGGREGATE_MAX_BYTES}-byte aggregate limit`,
|
|
396
|
+
details: { limitBytes: BINARY_AGGREGATE_MAX_BYTES },
|
|
397
|
+
}))
|
|
398
|
+
break
|
|
399
|
+
}
|
|
400
|
+
findings.push(...scanBinaryBuffer({ buffer: result.stdout, filePath, rules, exceptions, repo }))
|
|
401
|
+
}
|
|
402
|
+
return { findings, diagnostics, bytes: totalBytes }
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function scanStagedRepository({
|
|
406
|
+
repoRoot,
|
|
407
|
+
rules = DEFAULT_CONTENT_RULES,
|
|
408
|
+
exceptions = [],
|
|
409
|
+
repo = null,
|
|
410
|
+
gitExecutable = 'git',
|
|
411
|
+
gitRunner = spawnSync,
|
|
412
|
+
} = {}) {
|
|
413
|
+
const acquired = stagedDiff(repoRoot, { gitExecutable, runner: gitRunner })
|
|
414
|
+
if (!acquired.ok) {
|
|
415
|
+
return {
|
|
416
|
+
findings: [],
|
|
417
|
+
diagnostics: [incompleteDiagnostic({ repo, message: 'staged evidence could not be acquired', details: { reason: acquired.code } })],
|
|
418
|
+
bytes: 0,
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const files = parseAddedContent(acquired.diff)
|
|
422
|
+
const binary = readBinaryChanges({ repoRoot, revision: ':', diff: acquired.diff, rules, exceptions, repo, gitExecutable, gitRunner })
|
|
423
|
+
return {
|
|
424
|
+
findings: [...scanAddedContent({ files, rules, exceptions, repo }), ...binary.findings],
|
|
425
|
+
diagnostics: binary.diagnostics,
|
|
426
|
+
bytes: acquired.bytes + binary.bytes,
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function scanPushUpdate({
|
|
431
|
+
repoRoot,
|
|
432
|
+
update,
|
|
433
|
+
rules = DEFAULT_CONTENT_RULES,
|
|
434
|
+
exceptions = [],
|
|
435
|
+
repo = null,
|
|
436
|
+
gitRunner = spawnSync,
|
|
437
|
+
} = {}) {
|
|
438
|
+
const acquired = diffForPushRange(repoRoot, update, { runner: gitRunner })
|
|
439
|
+
if (!acquired.ok) {
|
|
440
|
+
return {
|
|
441
|
+
findings: [],
|
|
442
|
+
diagnostics: [incompleteDiagnostic({ repo, message: 'push evidence could not be acquired', details: { reason: acquired.code } })],
|
|
443
|
+
bytes: 0,
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const files = parseAddedContent(acquired.diff)
|
|
447
|
+
const binary = readBinaryChanges({ repoRoot, revision: update.localSha, diff: acquired.diff, rules, exceptions, repo })
|
|
448
|
+
return {
|
|
449
|
+
findings: [...scanAddedContent({ files, rules, exceptions, repo }), ...binary.findings],
|
|
450
|
+
diagnostics: binary.diagnostics,
|
|
451
|
+
bytes: acquired.bytes + binary.bytes,
|
|
452
|
+
}
|
|
242
453
|
}
|
|
243
454
|
|
|
244
455
|
/**
|
|
@@ -246,17 +457,69 @@ export function stagedDiff(repoRoot) {
|
|
|
246
457
|
* accepted usage, so a repo can see what it has taken on without those findings
|
|
247
458
|
* stopping unrelated work from being pushed.
|
|
248
459
|
*/
|
|
249
|
-
export function scanTree(repoRoot, {
|
|
250
|
-
|
|
460
|
+
export function scanTree(repoRoot, {
|
|
461
|
+
rules = DEFAULT_CONTENT_RULES,
|
|
462
|
+
exceptions = [],
|
|
463
|
+
repo = null,
|
|
464
|
+
source = 'working-tree',
|
|
465
|
+
} = {}) {
|
|
466
|
+
if (!['working-tree', 'head'].includes(source)) {
|
|
467
|
+
return { findings: [], diagnostics: [incompleteDiagnostic({ repo, message: `unknown audit source ${source}` })], source }
|
|
468
|
+
}
|
|
469
|
+
const listed = source === 'head'
|
|
470
|
+
? gitOutputResult(repoRoot, ['ls-tree', '-r', '--name-only', '-z', 'HEAD'])
|
|
471
|
+
: gitOutputResult(repoRoot, ['ls-files', '-co', '--exclude-standard', '-z'])
|
|
472
|
+
if (!listed.ok) {
|
|
473
|
+
return { findings: [], diagnostics: [incompleteDiagnostic({ repo, message: `${source} audit paths could not be acquired`, details: { reason: listed.code } })], source }
|
|
474
|
+
}
|
|
251
475
|
const files = []
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
476
|
+
const findings = []
|
|
477
|
+
const diagnostics = []
|
|
478
|
+
let totalBytes = 0
|
|
479
|
+
for (const rel of listed.stdout.split('\0').filter(Boolean)) {
|
|
480
|
+
let blob
|
|
481
|
+
if (source === 'head') {
|
|
482
|
+
const result = gitOutputResult(repoRoot, ['show', `HEAD:${rel}`], { encoding: 'buffer', maxBuffer: BINARY_FILE_MAX_BYTES + 1 })
|
|
483
|
+
if (!result.ok) {
|
|
484
|
+
diagnostics.push(incompleteDiagnostic({ repo, path: rel, message: 'HEAD blob could not be read', details: { reason: result.code } }))
|
|
485
|
+
continue
|
|
486
|
+
}
|
|
487
|
+
blob = result.stdout
|
|
488
|
+
} else {
|
|
489
|
+
const abs = pathForAudit(repoRoot, rel)
|
|
490
|
+
try {
|
|
491
|
+
const stat = fs.lstatSync(abs)
|
|
492
|
+
if (!stat.isFile() && !stat.isSymbolicLink()) continue
|
|
493
|
+
blob = stat.isSymbolicLink() ? Buffer.from(fs.readlinkSync(abs)) : fs.readFileSync(abs)
|
|
494
|
+
} catch {
|
|
495
|
+
diagnostics.push(incompleteDiagnostic({ repo, path: rel, message: 'working-tree file could not be read' }))
|
|
496
|
+
continue
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (blob.length > BINARY_FILE_MAX_BYTES) {
|
|
500
|
+
diagnostics.push(incompleteDiagnostic({ repo, path: rel, message: `audit content exceeds the ${BINARY_FILE_MAX_BYTES}-byte per-file limit` }))
|
|
501
|
+
continue
|
|
502
|
+
}
|
|
503
|
+
totalBytes += blob.length
|
|
504
|
+
if (totalBytes > BINARY_AGGREGATE_MAX_BYTES) {
|
|
505
|
+
diagnostics.push(incompleteDiagnostic({ repo, path: rel, message: `audit content exceeds the ${BINARY_AGGREGATE_MAX_BYTES}-byte aggregate limit` }))
|
|
506
|
+
break
|
|
507
|
+
}
|
|
508
|
+
if (isBinaryBuffer(blob)) {
|
|
509
|
+
findings.push(...scanBinaryBuffer({ buffer: blob, filePath: rel, rules, exceptions, repo }))
|
|
510
|
+
continue
|
|
511
|
+
}
|
|
512
|
+
const text = blob.toString('utf8')
|
|
255
513
|
files.push({
|
|
256
514
|
path: rel,
|
|
257
515
|
added: true,
|
|
258
|
-
addedLines:
|
|
516
|
+
addedLines: text.split('\n').map((line, index) => ({ number: index + 1, text: line })),
|
|
259
517
|
})
|
|
260
518
|
}
|
|
261
|
-
|
|
519
|
+
findings.push(...scanAddedContent({ files, rules, exceptions, repo }))
|
|
520
|
+
return { findings, diagnostics, source, bytes: totalBytes }
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function pathForAudit(repoRoot, rel) {
|
|
524
|
+
return path.join(repoRoot, ...normalizeRelPath(rel).split('/'))
|
|
262
525
|
}
|