@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
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
export const STRUCTURAL_DISCLOSURE_PATTERNS = Object.freeze([
|
|
6
|
+
{ pattern: new RegExp('\\/Users\\/'), label: 'absolute user path' },
|
|
7
|
+
{ pattern: new RegExp('\\/var\\/folders\\/'), label: 'machine-local temp path' },
|
|
8
|
+
{ pattern: new RegExp('\\.' + 'codex'), label: 'agent-local state path' },
|
|
9
|
+
{
|
|
10
|
+
pattern: new RegExp([
|
|
11
|
+
'BEGIN ',
|
|
12
|
+
'(?:[A-Z0-9]+ ){0,4}',
|
|
13
|
+
'PRIVATE KEY(?: BLOCK)?',
|
|
14
|
+
'|BEGIN (?:RSA|OPENSSH) KEY',
|
|
15
|
+
].join('')),
|
|
16
|
+
label: 'private key material',
|
|
17
|
+
},
|
|
18
|
+
{ pattern: /"d"\s*:\s*"[A-Za-z0-9_-]{20,}"/, label: 'JWK private key material' },
|
|
19
|
+
{
|
|
20
|
+
pattern: /\b(api[_-]?key|secret|password|(?<!id-)token)\b\s*[:=]/i,
|
|
21
|
+
label: 'secret-like assignment',
|
|
22
|
+
},
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
export function compileDisclosurePatterns(patternDocs = []) {
|
|
26
|
+
if (!Array.isArray(patternDocs)) throw new Error('denylist document must contain a patterns array')
|
|
27
|
+
return patternDocs.map(({ pattern, flags = '', label }) => {
|
|
28
|
+
if (typeof label !== 'string' || label.length === 0) throw new Error('denylist pattern label must be non-empty')
|
|
29
|
+
if (typeof pattern !== 'string' || pattern.length === 0) throw new Error(`denylist pattern failed to compile: ${label}`)
|
|
30
|
+
try {
|
|
31
|
+
return { pattern: new RegExp(pattern, String(flags).replace(/[gy]/g, '')), label }
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error(`denylist pattern failed to compile: ${label}`)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function scanDisclosureContent({
|
|
39
|
+
root = process.cwd(),
|
|
40
|
+
staged = false,
|
|
41
|
+
denylistPatterns = [],
|
|
42
|
+
failOnBinary = false,
|
|
43
|
+
} = {}) {
|
|
44
|
+
const resolvedRoot = path.resolve(root)
|
|
45
|
+
assertGitCheckout(resolvedRoot)
|
|
46
|
+
const patterns = [...STRUCTURAL_DISCLOSURE_PATTERNS, ...denylistPatterns]
|
|
47
|
+
const findings = []
|
|
48
|
+
const skippedBinary = []
|
|
49
|
+
let scannedFiles = 0
|
|
50
|
+
|
|
51
|
+
for (const entry of stagedEntries(resolvedRoot, staged)) {
|
|
52
|
+
const buffer = entry.buffer ?? readTrackedFile(resolvedRoot, entry.path)
|
|
53
|
+
if (buffer === null) continue
|
|
54
|
+
const text = decodeText(buffer)
|
|
55
|
+
if (text === null) {
|
|
56
|
+
skippedBinary.push(entry.path)
|
|
57
|
+
if (failOnBinary) findings.push({ label: 'binary content cannot be disclosure-scanned', path: entry.path, line: null })
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
scannedFiles += 1
|
|
62
|
+
const lines = text.split('\n')
|
|
63
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
64
|
+
for (const { pattern, label } of patterns) {
|
|
65
|
+
if (pattern.test(lines[index])) findings.push({ label, path: entry.path, line: index + 1 })
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
ok: findings.length === 0,
|
|
72
|
+
staged,
|
|
73
|
+
scannedFiles,
|
|
74
|
+
skippedBinary,
|
|
75
|
+
findings,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function decodeText(buffer) {
|
|
80
|
+
if (buffer.includes(0)) return null
|
|
81
|
+
try {
|
|
82
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(buffer)
|
|
83
|
+
} catch {
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function isPathTracked(root, candidatePath) {
|
|
89
|
+
const resolvedRoot = path.resolve(root)
|
|
90
|
+
const resolvedCandidate = path.resolve(candidatePath)
|
|
91
|
+
const relative = path.relative(resolvedRoot, resolvedCandidate)
|
|
92
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) return false
|
|
93
|
+
const gitRelative = relative.split(path.sep).join('/')
|
|
94
|
+
const result = execFileSync('git', ['-C', resolvedRoot, 'ls-files', '-z', '--', gitRelative], {
|
|
95
|
+
encoding: 'utf8',
|
|
96
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
97
|
+
})
|
|
98
|
+
if (result.split('\0').filter(Boolean).includes(gitRelative)) return true
|
|
99
|
+
|
|
100
|
+
// Git pathspec matching is case-sensitive even when the checkout filesystem
|
|
101
|
+
// is not. Compare filesystem identities so alternate-cased callers cannot
|
|
102
|
+
// make a tracked file appear local and untracked.
|
|
103
|
+
return execFileSync('git', [
|
|
104
|
+
'-C',
|
|
105
|
+
resolvedRoot,
|
|
106
|
+
'ls-files',
|
|
107
|
+
'-z',
|
|
108
|
+
'--',
|
|
109
|
+
`:(icase,literal)${gitRelative}`,
|
|
110
|
+
], {
|
|
111
|
+
encoding: 'utf8',
|
|
112
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
113
|
+
})
|
|
114
|
+
.split('\0')
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.some((trackedPath) => sameExistingFile(resolvedCandidate, path.join(resolvedRoot, trackedPath)))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function sameExistingFile(left, right) {
|
|
120
|
+
try {
|
|
121
|
+
if (fs.realpathSync.native(left) === fs.realpathSync.native(right)) return true
|
|
122
|
+
const leftStat = fs.statSync(left)
|
|
123
|
+
const rightStat = fs.statSync(right)
|
|
124
|
+
return leftStat.ino !== 0 && leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino
|
|
125
|
+
} catch {
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function isPathIgnored(root, candidatePath) {
|
|
131
|
+
const resolvedRoot = path.resolve(root)
|
|
132
|
+
const relative = path.relative(resolvedRoot, path.resolve(candidatePath))
|
|
133
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) return false
|
|
134
|
+
const gitRelative = relative.split(path.sep).join('/')
|
|
135
|
+
try {
|
|
136
|
+
execFileSync('git', ['-C', resolvedRoot, 'check-ignore', '--quiet', '--', gitRelative], {
|
|
137
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
138
|
+
})
|
|
139
|
+
return true
|
|
140
|
+
} catch {
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function assertGitCheckout(root) {
|
|
146
|
+
try {
|
|
147
|
+
execFileSync('git', ['-C', root, 'rev-parse', '--git-dir'], {
|
|
148
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
149
|
+
})
|
|
150
|
+
} catch {
|
|
151
|
+
throw new Error(`disclosure root is not a Git checkout`)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function stagedEntries(root, staged) {
|
|
156
|
+
if (!staged) {
|
|
157
|
+
return git(root, ['ls-files', '-z'])
|
|
158
|
+
.split('\0')
|
|
159
|
+
.filter(Boolean)
|
|
160
|
+
.map((filePath) => ({ path: filePath }))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return git(root, ['diff', '--cached', '--name-only', '--diff-filter=ACMR', '-z'])
|
|
164
|
+
.split('\0')
|
|
165
|
+
.filter(Boolean)
|
|
166
|
+
.map((filePath) => ({
|
|
167
|
+
path: filePath,
|
|
168
|
+
buffer: execFileSync('git', ['-C', root, 'show', `:${filePath}`], {
|
|
169
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
170
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
171
|
+
}),
|
|
172
|
+
}))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function readTrackedFile(root, relativePath) {
|
|
176
|
+
const absolutePath = path.join(root, relativePath)
|
|
177
|
+
let stat
|
|
178
|
+
try {
|
|
179
|
+
stat = fs.lstatSync(absolutePath)
|
|
180
|
+
} catch {
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
if (!stat.isFile()) return null
|
|
184
|
+
return fs.readFileSync(absolutePath)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function git(root, args) {
|
|
188
|
+
return execFileSync('git', ['-C', root, ...args], {
|
|
189
|
+
encoding: 'utf8',
|
|
190
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
191
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
192
|
+
})
|
|
193
|
+
}
|
package/src/egress/check.mjs
CHANGED
|
@@ -1,44 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from 'node:path'
|
|
1
|
+
import { checkForbiddenEgress } from './forbidden-egress.mjs'
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
const SCAN_DIRS = ['bin', 'src']
|
|
6
|
-
const FORBIDDEN = [
|
|
7
|
-
{ pattern: /fetch\(\s*['"]https?:\/\/(?!127\.0\.0\.1|localhost|\[::1\])/, label: 'non-localhost fetch' },
|
|
8
|
-
{ pattern: /https?\.request\s*\(/, label: 'http request primitive' },
|
|
9
|
-
{ pattern: /new\s+WebSocket\s*\(/, label: 'websocket egress primitive' },
|
|
10
|
-
{ pattern: /net\.connect\s*\(/, label: 'net connect primitive' },
|
|
11
|
-
{ pattern: /dns\./, label: 'dns primitive' },
|
|
12
|
-
{ pattern: /\bcurl\s+https?:\/\//, label: 'shell network call' },
|
|
13
|
-
]
|
|
14
|
-
|
|
15
|
-
function walk(dir, files = []) {
|
|
16
|
-
if (!fs.existsSync(dir)) return files
|
|
17
|
-
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
18
|
-
const abs = path.join(dir, ent.name)
|
|
19
|
-
if (ent.isDirectory()) walk(abs, files)
|
|
20
|
-
else if (/\.(mjs|js|sh)$/.test(ent.name)) files.push(abs)
|
|
21
|
-
}
|
|
22
|
-
return files
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function checkForbiddenEgress(root = ROOT) {
|
|
26
|
-
const failures = []
|
|
27
|
-
for (const rel of SCAN_DIRS) {
|
|
28
|
-
for (const file of walk(path.join(root, rel))) {
|
|
29
|
-
const text = fs.readFileSync(file, 'utf8')
|
|
30
|
-
for (const rule of FORBIDDEN) {
|
|
31
|
-
if (rule.pattern.test(text)) failures.push(`${path.relative(root, file)}:${rule.label}`)
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return failures
|
|
36
|
-
}
|
|
3
|
+
export { checkForbiddenEgress } from './forbidden-egress.mjs'
|
|
37
4
|
|
|
38
5
|
export function runEgressCommand() {
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
|
|
6
|
+
const findings = checkForbiddenEgress()
|
|
7
|
+
if (findings.length) {
|
|
8
|
+
for (const finding of findings) {
|
|
9
|
+
console.error(`[atelier-egress] ${finding.file}:${finding.line} ${finding.type}: ${finding.detail}`)
|
|
10
|
+
}
|
|
42
11
|
process.exit(1)
|
|
43
12
|
}
|
|
44
13
|
console.log('[egress:check] no forbidden non-localhost egress found in package runtime paths')
|
|
@@ -22,20 +22,21 @@ export const DEFAULT_EGRESS_SCAN_PATHS = [
|
|
|
22
22
|
const SCRIPT_EXTS = new Set(['.js', '.mjs', '.cjs', '.ts', '.mts', '.sh'])
|
|
23
23
|
const MARKUP_EXTS = new Set(['.html', '.htm', '.svg'])
|
|
24
24
|
const SKIP_DIRS = new Set(['node_modules', '.git'])
|
|
25
|
-
const
|
|
25
|
+
const INTERNAL_SCANNER_PATH = 'src/egress/forbidden-egress.mjs'
|
|
26
26
|
|
|
27
27
|
function isTestFile(file) {
|
|
28
28
|
return /(?:^|[/.])test(?:s)?[/.]/.test(file) || /\.(?:test|spec)\.[cm]?[jt]s$/.test(file)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function isMarkedTestFixture(file, text) {
|
|
32
|
-
return isTestFile(file) && text.includes(TEST_FIXTURE_ALLOW_MARKER)
|
|
31
|
+
function isMarkedTestFixture(file, text, allowTestFixtures) {
|
|
32
|
+
return allowTestFixtures && isTestFile(file) && text.includes(TEST_FIXTURE_ALLOW_MARKER)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function shouldScanFile(file, { includeTests =
|
|
35
|
+
function shouldScanFile(file, { includeTests = true, root = packageRoot } = {}) {
|
|
36
36
|
const ext = path.extname(file)
|
|
37
37
|
if (!SCRIPT_EXTS.has(ext) && !MARKUP_EXTS.has(ext)) return false
|
|
38
|
-
|
|
38
|
+
const relative = path.relative(path.resolve(root), path.resolve(file)).split(path.sep).join('/')
|
|
39
|
+
if (relative === INTERNAL_SCANNER_PATH) return false
|
|
39
40
|
if (!includeTests && isTestFile(file)) return false
|
|
40
41
|
return true
|
|
41
42
|
}
|
|
@@ -61,17 +62,28 @@ function uniqueFiles(files) {
|
|
|
61
62
|
export function discoverForbiddenEgressScanFiles({
|
|
62
63
|
root = packageRoot,
|
|
63
64
|
scanPaths = DEFAULT_EGRESS_SCAN_PATHS,
|
|
64
|
-
includeTests =
|
|
65
|
+
includeTests = true,
|
|
66
|
+
files = null,
|
|
65
67
|
} = {}) {
|
|
66
|
-
|
|
68
|
+
if (Array.isArray(files)) {
|
|
69
|
+
const resolvedRoot = path.resolve(root)
|
|
70
|
+
return uniqueFiles(
|
|
71
|
+
files
|
|
72
|
+
.map((file) => path.resolve(resolvedRoot, file))
|
|
73
|
+
.filter((file) => file === resolvedRoot || file.startsWith(`${resolvedRoot}${path.sep}`))
|
|
74
|
+
.filter((file) => fs.existsSync(file) && fs.statSync(file).isFile())
|
|
75
|
+
.filter((file) => shouldScanFile(file, { includeTests, root: resolvedRoot })),
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
const discovered = []
|
|
67
79
|
for (const rel of scanPaths) {
|
|
68
80
|
const abs = path.resolve(root, rel)
|
|
69
81
|
if (!fs.existsSync(abs)) continue
|
|
70
82
|
const stat = fs.statSync(abs)
|
|
71
|
-
if (stat.isDirectory())
|
|
72
|
-
else if (shouldScanFile(abs, { includeTests }))
|
|
83
|
+
if (stat.isDirectory()) discovered.push(...walk(abs, { includeTests, root }))
|
|
84
|
+
else if (shouldScanFile(abs, { includeTests, root })) discovered.push(abs)
|
|
73
85
|
}
|
|
74
|
-
return uniqueFiles(
|
|
86
|
+
return uniqueFiles(discovered)
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
function hostnameFromNetworkUrl(value) {
|
|
@@ -135,8 +147,8 @@ function callBlock(lines, index) {
|
|
|
135
147
|
return lines.slice(index, Math.min(lines.length, index + 5)).join('\n')
|
|
136
148
|
}
|
|
137
149
|
|
|
138
|
-
function isFixtureAllowed(lines, index, file) {
|
|
139
|
-
return isTestFile(file) && callBlock(lines, Math.max(0, index - 2)).includes(TEST_FIXTURE_ALLOW_MARKER)
|
|
150
|
+
function isFixtureAllowed(lines, index, file, allowTestFixtures) {
|
|
151
|
+
return allowTestFixtures && isTestFile(file) && callBlock(lines, Math.max(0, index - 2)).includes(TEST_FIXTURE_ALLOW_MARKER)
|
|
140
152
|
}
|
|
141
153
|
|
|
142
154
|
function isLocalComputedAllowed(lines, index) {
|
|
@@ -256,14 +268,14 @@ function markupFindingsForLine(line, { findings, file, lineNumber }) {
|
|
|
256
268
|
}
|
|
257
269
|
}
|
|
258
270
|
|
|
259
|
-
export function forbiddenEgressFindingsForText(text, { file = 'input' } = {}) {
|
|
260
|
-
if (isMarkedTestFixture(file, text)) return []
|
|
271
|
+
export function forbiddenEgressFindingsForText(text, { file = 'input', allowTestFixtures = true } = {}) {
|
|
272
|
+
if (isMarkedTestFixture(file, text, allowTestFixtures)) return []
|
|
261
273
|
const findings = []
|
|
262
274
|
const lines = String(text || '').split(/\r?\n/)
|
|
263
275
|
const isMarkup = MARKUP_EXTS.has(path.extname(file))
|
|
264
276
|
|
|
265
277
|
for (let index = 0; index < lines.length; index += 1) {
|
|
266
|
-
if (isFixtureAllowed(lines, index, file)) continue
|
|
278
|
+
if (isFixtureAllowed(lines, index, file, allowTestFixtures)) continue
|
|
267
279
|
const lineCode = trimmedCodeLine(lines[index])
|
|
268
280
|
const hasJsPrimitive = /\b(?:fetch|https?\.(?:request|get)|http2\.connect|navigator\.sendBeacon|new\s+EventSource|new\s+WebSocket|net\.connect|net\.createConnection|dns\.(?:resolve|lookup|promises\.resolve|promises\.lookup)|import)\s*\(/.test(lineCode)
|
|
269
281
|
|| /\bnew\s+(?:XMLHttpRequest|Image)\b/.test(lineCode)
|
|
@@ -295,12 +307,14 @@ export function forbiddenEgressFindingsForText(text, { file = 'input' } = {}) {
|
|
|
295
307
|
export function checkForbiddenEgress({
|
|
296
308
|
root = packageRoot,
|
|
297
309
|
scanPaths = DEFAULT_EGRESS_SCAN_PATHS,
|
|
298
|
-
includeTests =
|
|
310
|
+
includeTests = true,
|
|
311
|
+
files = null,
|
|
312
|
+
allowTestFixtures = true,
|
|
299
313
|
} = {}) {
|
|
300
314
|
const findings = []
|
|
301
|
-
for (const file of discoverForbiddenEgressScanFiles({ root, scanPaths, includeTests })) {
|
|
315
|
+
for (const file of discoverForbiddenEgressScanFiles({ root, scanPaths, includeTests, files })) {
|
|
302
316
|
const rel = path.relative(root, file)
|
|
303
|
-
findings.push(...forbiddenEgressFindingsForText(fs.readFileSync(file, 'utf8'), { file: rel }))
|
|
317
|
+
findings.push(...forbiddenEgressFindingsForText(fs.readFileSync(file, 'utf8'), { file: rel, allowTestFixtures }))
|
|
304
318
|
}
|
|
305
319
|
return findings
|
|
306
320
|
}
|