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