mustflow 2.103.10 → 2.103.12

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.
@@ -10,7 +10,7 @@ const CODE_NAVIGATION_SCRIPT_REFS = new Set([
10
10
  'repo/related-files',
11
11
  ]);
12
12
  const CONFIG_CHAIN_SURFACES = new Set(['config', 'package', 'source', 'test']);
13
- const CONFIG_FILE_PATTERN = /(?:^|\/)(?:\.gitignore|\.env\.(?:example|sample|template|defaults)|\.dev\.vars\.example|tsconfig(?:\..*)?\.json|eslint\.config\.[cm]?[jt]s|\.eslintrc(?:\.json)?|\.prettierrc(?:\.json)?|prettier\.config\.[cm]?[jt]s|vite\.config\.[cm]?[jt]s|vitest\.config\.[cm]?[jt]s|tailwind\.config\.[cm]?[jt]s|jest\.config\.[cm]?[jt]s|playwright\.config\.[cm]?[jt]s|astro\.config\.mjs|svelte\.config\.js)$/u;
13
+ const CONFIG_FILE_PATTERN = /(?:^|\/)(?:\.gitignore|\.env\.(?:example|sample|template|defaults)|\.dev\.vars\.example|tsconfig(?:\..*)?\.json|eslint\.config\.[cm]?[jt]s|\.eslintrc(?:\.json)?|\.prettierrc(?:\.json)?|prettier\.config\.[cm]?[jt]s|vite\.config\.[cm]?[jt]s|vitest\.config\.[cm]?[jt]s|tailwind\.config\.[cm]?[jt]s|jest\.config\.[cm]?[jt]s|playwright\.config\.[cm]?[jt]s|astro\.config\.mjs|svelte\.config\.js|wrangler\.(?:toml|jsonc?)|vercel\.json|netlify\.toml|Dockerfile|docker-compose\.ya?ml|compose\.ya?ml)$/u;
14
14
  export function isScriptPackSuggestionPhase(value) {
15
15
  return ['before_change', 'during_change', 'after_change', 'review'].includes(value);
16
16
  }
@@ -133,7 +133,7 @@ function surfacesForScript(script) {
133
133
  addIf('skill', /skill|workflow/u);
134
134
  addIf('generated', /generated|protected|vendor|cache|boundary/u);
135
135
  addIf('config', /config|command/u);
136
- addIf('package', /package|release/u);
136
+ addIf('package', /deploy|package|publish|release/u);
137
137
  addIf('test', /test|suite|fixture|coverage|selection|timing|performance/u);
138
138
  addIf('source', /code|source|symbol/u);
139
139
  if (script.ref === 'repo/manifest-lock-drift') {
@@ -265,6 +265,9 @@ function createRunHint(script, analyzedPaths) {
265
265
  if (script.ref === 'repo/approval-gate') {
266
266
  return 'mf script-pack run repo/approval-gate check --action <action_type> --json';
267
267
  }
268
+ if (script.ref === 'repo/deploy-surface') {
269
+ return 'mf script-pack run repo/deploy-surface inspect --json';
270
+ }
268
271
  if (script.ref === 'repo/config-chain') {
269
272
  const configPaths = analyzedPaths
270
273
  .filter((entry) => entry.surfaces.some((surface) => CONFIG_CHAIN_SURFACES.has(surface)))
@@ -283,6 +286,12 @@ function createRunHint(script, analyzedPaths) {
283
286
  .map((entry) => entry.path);
284
287
  return createConcretePathHint('mf script-pack run repo/secret-risk-scan scan', secretRiskPaths, script.usage);
285
288
  }
289
+ if (script.ref === 'repo/security-pattern-scan') {
290
+ const securityPatternPaths = analyzedPaths
291
+ .filter((entry) => entry.surfaces.some((surface) => surface === 'config' || surface === 'source' || surface === 'package' || surface === 'test'))
292
+ .map((entry) => entry.path);
293
+ return createConcretePathHint('mf script-pack run repo/security-pattern-scan scan', securityPatternPaths, script.usage);
294
+ }
286
295
  if (script.ref === 'repo/related-files') {
287
296
  const relatedPaths = analyzedPaths
288
297
  .filter((entry) => entry.surfaces.some((surface) => surface === 'source' || surface === 'test'))
@@ -367,6 +376,24 @@ export function createScriptPackSuggestionReport(mustflowRoot, options) {
367
376
  score += 2;
368
377
  reasons.push('Prioritizes approval-gate checks for approval-sensitive workflow and release surfaces.');
369
378
  }
379
+ if (script.ref === 'repo/deploy-surface' &&
380
+ (requestedSurfaces.has('package') || requestedSurfaces.has('config') || requestedSurfaces.has('docs'))) {
381
+ score += 2;
382
+ reasons.push('Prioritizes deploy-surface inspection for push, tag, release, docs, and package publication follow-up.');
383
+ }
384
+ if (script.ref === 'repo/security-pattern-scan' &&
385
+ (hasSourcePath ||
386
+ requestedSurfaces.has('config') ||
387
+ options.skills.some((skill) => [
388
+ 'api-access-control-review',
389
+ 'file-upload-security-review',
390
+ 'security-flow-review',
391
+ 'security-privacy-review',
392
+ 'security-regression-tests',
393
+ ].includes(skill)))) {
394
+ score += 2;
395
+ reasons.push('Prioritizes security-pattern scans for source, config, and security-review surfaces.');
396
+ }
370
397
  if (score === 0) {
371
398
  return null;
372
399
  }
@@ -0,0 +1,518 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, lstatSync, readdirSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
5
+ import { ensureInside, ensureInsideWithoutSymlinks, readUtf8FileInsideWithoutSymlinks } from './safe-filesystem.js';
6
+ export const SECURITY_PATTERN_SCAN_PACK_ID = 'repo';
7
+ export const SECURITY_PATTERN_SCAN_SCRIPT_ID = 'security-pattern-scan';
8
+ export const SECURITY_PATTERN_SCAN_SCRIPT_REF = `${SECURITY_PATTERN_SCAN_PACK_ID}/${SECURITY_PATTERN_SCAN_SCRIPT_ID}`;
9
+ const DEFAULT_MAX_FILE_BYTES = 256 * 1024;
10
+ const DEFAULT_MAX_FILES = 1000;
11
+ const DEFAULT_MAX_FINDINGS = 300;
12
+ const MAX_ISSUES = 50;
13
+ const SCAN_EXTENSIONS = [
14
+ '.c',
15
+ '.cc',
16
+ '.cpp',
17
+ '.cs',
18
+ '.cts',
19
+ '.go',
20
+ '.java',
21
+ '.js',
22
+ '.jsx',
23
+ '.kt',
24
+ '.md',
25
+ '.mdx',
26
+ '.mjs',
27
+ '.mts',
28
+ '.php',
29
+ '.py',
30
+ '.rb',
31
+ '.rs',
32
+ '.sh',
33
+ '.ts',
34
+ '.tsx',
35
+ '.yaml',
36
+ '.yml',
37
+ ];
38
+ const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
39
+ const ERROR_CODES = new Set([
40
+ 'security_pattern_path_outside_root',
41
+ 'security_pattern_unreadable_path',
42
+ ]);
43
+ function normalizeRelativePath(value) {
44
+ return value.replace(/\\/gu, '/').replace(/^\.\/+/u, '') || '.';
45
+ }
46
+ function sha256Tagged(value) {
47
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
48
+ }
49
+ function fingerprint(value) {
50
+ return `sha256:${createHash('sha256').update(value).digest('hex').slice(0, 16)}`;
51
+ }
52
+ function pushIssue(issues, issue) {
53
+ if (issues.length < MAX_ISSUES) {
54
+ issues.push(issue);
55
+ }
56
+ }
57
+ function makeFinding(code, severity, pathValue, message, details = {}) {
58
+ return {
59
+ code,
60
+ severity,
61
+ path: pathValue,
62
+ message,
63
+ line: details.line,
64
+ detector: details.detector,
65
+ category: details.category,
66
+ review_focus: details.reviewFocus,
67
+ fingerprint: details.fingerprint,
68
+ json_pointer: null,
69
+ metric: null,
70
+ actual: null,
71
+ expected: null,
72
+ };
73
+ }
74
+ function positiveInteger(value, fallback) {
75
+ return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : fallback;
76
+ }
77
+ function isIgnoredDirectory(relativePath) {
78
+ return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), IGNORED_DIRECTORIES);
79
+ }
80
+ function surfaceForPath(relativePath) {
81
+ const normalized = normalizeRelativePath(relativePath);
82
+ const extension = path.extname(normalized).toLowerCase();
83
+ if (!SCAN_EXTENSIONS.includes(extension)) {
84
+ return null;
85
+ }
86
+ if (normalized.startsWith('.github/workflows/') || ['.yml', '.yaml'].includes(extension)) {
87
+ return 'ci';
88
+ }
89
+ if (['.md', '.mdx'].includes(extension)) {
90
+ return 'docs';
91
+ }
92
+ if (['.json', '.toml', '.yaml', '.yml'].includes(extension) || normalized.startsWith('.mustflow/config/')) {
93
+ return 'config';
94
+ }
95
+ return 'code';
96
+ }
97
+ function normalizeTargetPath(projectRoot, targetPath) {
98
+ const absolutePath = path.resolve(process.cwd(), targetPath);
99
+ ensureInside(projectRoot, absolutePath);
100
+ return {
101
+ absolutePath,
102
+ relativePath: normalizeRelativePath(path.relative(projectRoot, absolutePath)),
103
+ };
104
+ }
105
+ function targetKind(absolutePath) {
106
+ if (!existsSync(absolutePath)) {
107
+ return { exists: false, kind: 'missing' };
108
+ }
109
+ const stats = lstatSync(absolutePath);
110
+ if (stats.isFile()) {
111
+ return { exists: true, kind: 'file' };
112
+ }
113
+ if (stats.isDirectory()) {
114
+ return { exists: true, kind: 'directory' };
115
+ }
116
+ return { exists: true, kind: 'other' };
117
+ }
118
+ function addCandidate(candidates, findings, issues, policy, candidate) {
119
+ if (candidates.has(candidate.relativePath)) {
120
+ return;
121
+ }
122
+ if (candidates.size >= policy.max_files) {
123
+ if (!findings.some((finding) => finding.code === 'security_pattern_max_files_exceeded')) {
124
+ const message = `Security-pattern scan matched more than ${policy.max_files} files; remaining files were skipped.`;
125
+ pushIssue(issues, message);
126
+ findings.push(makeFinding('security_pattern_max_files_exceeded', 'medium', candidate.relativePath, message));
127
+ }
128
+ return;
129
+ }
130
+ candidates.set(candidate.relativePath, candidate);
131
+ }
132
+ function collectFilesFromDirectory(projectRoot, absoluteDirectory, candidates, findings, issues, policy) {
133
+ const relativeDirectory = normalizeRelativePath(path.relative(projectRoot, absoluteDirectory));
134
+ if (isIgnoredDirectory(relativeDirectory)) {
135
+ return;
136
+ }
137
+ let entries;
138
+ try {
139
+ ensureInsideWithoutSymlinks(projectRoot, absoluteDirectory);
140
+ entries = readdirSync(absoluteDirectory, { withFileTypes: true });
141
+ }
142
+ catch (error) {
143
+ const message = error instanceof Error ? error.message : String(error);
144
+ pushIssue(issues, `${relativeDirectory}: ${message}`);
145
+ findings.push(makeFinding('security_pattern_unreadable_path', 'high', relativeDirectory, message));
146
+ return;
147
+ }
148
+ for (const entry of entries) {
149
+ const absoluteEntry = path.join(absoluteDirectory, entry.name);
150
+ const relativeEntry = normalizeRelativePath(path.relative(projectRoot, absoluteEntry));
151
+ if (entry.isDirectory()) {
152
+ collectFilesFromDirectory(projectRoot, absoluteEntry, candidates, findings, issues, policy);
153
+ continue;
154
+ }
155
+ if (!entry.isFile()) {
156
+ continue;
157
+ }
158
+ const surface = surfaceForPath(relativeEntry);
159
+ if (surface) {
160
+ addCandidate(candidates, findings, issues, policy, { absolutePath: absoluteEntry, relativePath: relativeEntry, surface });
161
+ }
162
+ }
163
+ }
164
+ function lineNumberAtIndex(text, index) {
165
+ let line = 1;
166
+ let offset = 0;
167
+ while (offset < index) {
168
+ if (text.charCodeAt(offset) === 10) {
169
+ line += 1;
170
+ }
171
+ offset += 1;
172
+ }
173
+ return line;
174
+ }
175
+ function addBoundedFinding(findings, issues, policy, finding) {
176
+ if (findings.length >= policy.max_findings) {
177
+ if (!findings.some((entry) => entry.code === 'security_pattern_max_findings_exceeded')) {
178
+ const message = `Security-pattern scan found more than ${policy.max_findings} findings; remaining findings were skipped.`;
179
+ pushIssue(issues, message);
180
+ findings.push(makeFinding('security_pattern_max_findings_exceeded', 'medium', finding.path, message));
181
+ }
182
+ return;
183
+ }
184
+ findings.push(finding);
185
+ }
186
+ const ALWAYS = () => true;
187
+ const CODE_ONLY = (candidate) => candidate.surface === 'code';
188
+ const CODE_OR_CI = (candidate) => candidate.surface === 'code' || candidate.surface === 'ci';
189
+ const RULES = [
190
+ {
191
+ code: 'security_pattern_fs_call_non_literal_path',
192
+ detector: 'fs_call_non_literal_path',
193
+ category: 'filesystem',
194
+ severity: 'medium',
195
+ message: 'Filesystem call receives a non-literal first argument.',
196
+ reviewFocus: 'Prove path normalization and allowed-root containment before trusting this file operation.',
197
+ pattern: /\b(?:fs\.)?(?:readFile|readFileSync|writeFile|writeFileSync|appendFile|createReadStream|createWriteStream|unlink|rm|rename|copyFile|mkdir|readdir|stat|lstat)\s*\(\s*(?!["'`])/gu,
198
+ appliesTo: CODE_ONLY,
199
+ },
200
+ {
201
+ code: 'security_pattern_path_join_user_input',
202
+ detector: 'path_join_user_input',
203
+ category: 'filesystem',
204
+ severity: 'high',
205
+ message: 'Path composition appears to use request-controlled input.',
206
+ reviewFocus: 'Trace the composed path to the file sink and prove real-path containment after decoding and symlink checks.',
207
+ pattern: /\bpath\.(?:join|resolve)\s*\([^;\n]*(?:req\.|request\.|ctx\.|params|body|query)/gu,
208
+ appliesTo: CODE_ONLY,
209
+ },
210
+ {
211
+ code: 'security_pattern_dynamic_regex',
212
+ detector: 'dynamic_regex',
213
+ category: 'injection',
214
+ severity: 'medium',
215
+ message: 'RegExp constructor receives a dynamic pattern.',
216
+ reviewFocus: 'Check whether attacker-controlled text can choose the pattern; prefer literals, escaping, length limits, or a safe regex engine.',
217
+ pattern: /\b(?:new\s+RegExp|RegExp)\s*\(\s*(?!["'`][^"'`\r\n]*["'`]\s*[,)]).+/gu,
218
+ appliesTo: CODE_ONLY,
219
+ },
220
+ {
221
+ code: 'security_pattern_shell_true',
222
+ detector: 'shell_true',
223
+ category: 'command',
224
+ severity: 'high',
225
+ message: 'Process spawn options enable shell execution.',
226
+ reviewFocus: 'Use argv arrays and a static executable map; prove user-controlled strings cannot reach shell syntax.',
227
+ pattern: /\bshell\s*:\s*true\b/gu,
228
+ appliesTo: CODE_OR_CI,
229
+ },
230
+ {
231
+ code: 'security_pattern_eval_execution',
232
+ detector: 'eval_execution',
233
+ category: 'injection',
234
+ severity: 'critical',
235
+ message: 'Dynamic code execution primitive found.',
236
+ reviewFocus: 'Replace eval/new Function with a bounded parser, static operation map, or sandboxed interpreter with explicit policy.',
237
+ pattern: /\b(?:eval\s*\(|new\s+Function\s*\(|Function\s*\()/gu,
238
+ appliesTo: CODE_ONLY,
239
+ },
240
+ {
241
+ code: 'security_pattern_sql_template_interpolation',
242
+ detector: 'sql_template_interpolation',
243
+ category: 'injection',
244
+ severity: 'high',
245
+ message: 'SQL-looking template string contains interpolation.',
246
+ reviewFocus: 'Use parameterized values and allowlisted identifiers; check ORDER BY, table, column, and raw ORM fragments separately.',
247
+ pattern: /`[^`\r\n]*(?:SELECT|UPDATE|DELETE|INSERT|WHERE|ORDER BY)[^`\r\n]*\$\{[^`\r\n]*`/giu,
248
+ appliesTo: CODE_ONLY,
249
+ },
250
+ {
251
+ code: 'security_pattern_mass_assignment',
252
+ detector: 'mass_assignment',
253
+ category: 'access_control',
254
+ severity: 'high',
255
+ message: 'Request body appears to be bound directly into an entity or persistence call.',
256
+ reviewFocus: 'Replace raw body binding with a write DTO allowlist and server-derived privileged fields.',
257
+ pattern: /\b(?:Object\.assign\s*\([^,\n]+,\s*(?:req|request|ctx)?\.?body\b|(?:create|update|updateMany|insert|save)\s*\(\s*(?:req|request|ctx)?\.?body\b)/gu,
258
+ appliesTo: CODE_ONLY,
259
+ },
260
+ {
261
+ code: 'security_pattern_client_controlled_authority',
262
+ detector: 'client_controlled_authority',
263
+ category: 'access_control',
264
+ severity: 'high',
265
+ message: 'Authority-bearing field appears to come from request-controlled input.',
266
+ reviewFocus: 'Derive user, tenant, role, price, plan, owner, and entitlement from trusted server state, not request fields.',
267
+ pattern: /\b(?:req|request|ctx)\.(?:body|query|headers|params)\.?(?:\[['"])?(?:userId|accountId|tenantId|orgId|workspaceId|ownerId|role|isAdmin|permissions|scope|plan|price|status|entitlement)(?:['"]\])?/giu,
268
+ appliesTo: CODE_ONLY,
269
+ },
270
+ {
271
+ code: 'security_pattern_insecure_cookie_options',
272
+ detector: 'insecure_cookie_options',
273
+ category: 'token_session',
274
+ severity: 'medium',
275
+ message: 'Cookie-setting call does not show secure session-cookie flags on the same line.',
276
+ reviewFocus: 'For authority-bearing cookies, verify HttpOnly, Secure, SameSite, path, lifetime, rotation, logout, and CSRF posture.',
277
+ pattern: /\b(?:res|response|ctx)\.cookie\s*\([^\r\n]*/gu,
278
+ appliesTo: CODE_ONLY,
279
+ linePredicate: (line) => !/httpOnly|secure|sameSite/iu.test(line),
280
+ },
281
+ {
282
+ code: 'security_pattern_cors_origin_reflection_with_credentials',
283
+ detector: 'cors_origin_reflection_with_credentials',
284
+ category: 'browser',
285
+ severity: 'high',
286
+ message: 'CORS origin reflection appears in a file that also enables credentials.',
287
+ reviewFocus: 'Replace reflected origins with an allowlist and emit Vary: Origin when credentials are allowed.',
288
+ pattern: /Access-Control-Allow-Origin[^;\n]*(?:req|request)\.headers\.origin|(?:req|request)\.headers\.origin[^;\n]*Access-Control-Allow-Origin/giu,
289
+ appliesTo: (_candidate, text) => /Access-Control-Allow-Credentials[^;\n]*true/iu.test(text),
290
+ },
291
+ {
292
+ code: 'security_pattern_postmessage_wildcard_target',
293
+ detector: 'postmessage_wildcard_target',
294
+ category: 'browser',
295
+ severity: 'high',
296
+ message: 'postMessage uses a wildcard target origin.',
297
+ reviewFocus: 'Send messages only to a fixed trusted origin and avoid sending tokens or secrets through cross-window messages.',
298
+ pattern: /\.postMessage\s*\([^;\n]*,\s*["']\*["']/gu,
299
+ appliesTo: CODE_ONLY,
300
+ },
301
+ {
302
+ code: 'security_pattern_postmessage_missing_origin_check',
303
+ detector: 'postmessage_missing_origin_check',
304
+ category: 'browser',
305
+ severity: 'medium',
306
+ message: 'Message event listener has no visible event.origin check in the file.',
307
+ reviewFocus: 'Validate event.origin, event.source, message type, and payload schema before acting on cross-window messages.',
308
+ pattern: /addEventListener\s*\(\s*["']message["']/gu,
309
+ appliesTo: (_candidate, text) => !/\bevent\.origin\b|\borigin\s*!==|\borigin\s*===/u.test(text),
310
+ },
311
+ {
312
+ code: 'security_pattern_local_storage_token',
313
+ detector: 'local_storage_token',
314
+ category: 'token_session',
315
+ severity: 'high',
316
+ message: 'Browser localStorage appears to store a token or session-like value.',
317
+ reviewFocus: 'Avoid durable browser-readable authority where possible; review XSS blast radius, token lifetime, rotation, and revocation.',
318
+ pattern: /\blocalStorage\.setItem\s*\([^;\n]*(?:token|session|jwt|auth|refresh|api[_-]?key)/giu,
319
+ appliesTo: CODE_ONLY,
320
+ },
321
+ {
322
+ code: 'security_pattern_server_fetch_user_url',
323
+ detector: 'server_fetch_user_url',
324
+ category: 'injection',
325
+ severity: 'high',
326
+ message: 'Server-side HTTP call appears to use a request-controlled URL.',
327
+ reviewFocus: 'Treat this as SSRF until scheme, host, redirects, DNS resolution, private networks, timeout, and size limits are proven.',
328
+ pattern: /\b(?:fetch|axios\.(?:get|post|put|patch)|request|got)\s*\([^;\n]*(?:req|request|ctx)\.(?:query|body|params)[^;\n]*(?:url|uri|href)/giu,
329
+ appliesTo: CODE_ONLY,
330
+ },
331
+ {
332
+ code: 'security_pattern_tls_verification_disabled',
333
+ detector: 'tls_verification_disabled',
334
+ category: 'crypto_transport',
335
+ severity: 'critical',
336
+ message: 'TLS certificate verification appears to be disabled.',
337
+ reviewFocus: 'Remove certificate-verification bypasses and use test-only injection or local trust roots when needed.',
338
+ pattern: /\b(?:rejectUnauthorized\s*:\s*false|InsecureSkipVerify\s*:\s*true|verify\s*=\s*False|check_hostname\s*=\s*False)/gu,
339
+ appliesTo: CODE_OR_CI,
340
+ },
341
+ {
342
+ code: 'security_pattern_unsafe_yaml_load',
343
+ detector: 'unsafe_yaml_load',
344
+ category: 'parser',
345
+ severity: 'high',
346
+ message: 'YAML parsing appears to use an unsafe loader.',
347
+ reviewFocus: 'Use safe_load or a schema-backed parser and validate the resulting data shape before use.',
348
+ pattern: /\byaml\.load\s*\([^;\n]*(?:Loader\s*=\s*yaml\.(?:Loader|FullLoader|UnsafeLoader)|FullLoader|UnsafeLoader)/gu,
349
+ appliesTo: CODE_ONLY,
350
+ },
351
+ {
352
+ code: 'security_pattern_native_deserialization',
353
+ detector: 'native_deserialization',
354
+ category: 'parser',
355
+ severity: 'critical',
356
+ message: 'Native object deserialization primitive found.',
357
+ reviewFocus: 'Do not deserialize untrusted bytes into executable or language-native objects; use JSON plus schema validation where possible.',
358
+ pattern: /\b(?:pickle\.loads|pickle\.load|marshal\.loads|ObjectInputStream|BinaryFormatter|yaml\.unsafe_load|bincode::deserialize)/gu,
359
+ appliesTo: CODE_ONLY,
360
+ },
361
+ {
362
+ code: 'security_pattern_raw_sensitive_request_logging',
363
+ detector: 'raw_sensitive_request_logging',
364
+ category: 'logging',
365
+ severity: 'high',
366
+ message: 'Logger call appears to include raw request headers, body, cookies, or query.',
367
+ reviewFocus: 'Redact Authorization, Cookie, token, OTP, password, secret, and reset-link fields before logging request metadata.',
368
+ pattern: /\b(?:logger|log|console)\.(?:debug|info|warn|error|log)\s*\([^;\n]*(?:req|request)\.(?:headers|body|cookies|query)/gu,
369
+ appliesTo: CODE_ONLY,
370
+ },
371
+ ];
372
+ function scanCandidate(projectRoot, candidate, policy, findings, issues) {
373
+ let text;
374
+ try {
375
+ text = readUtf8FileInsideWithoutSymlinks(projectRoot, candidate.absolutePath, { maxBytes: policy.max_file_bytes });
376
+ }
377
+ catch (error) {
378
+ const message = error instanceof Error ? error.message : String(error);
379
+ const code = message.includes('exceeds maximum size')
380
+ ? 'security_pattern_file_too_large'
381
+ : 'security_pattern_unreadable_path';
382
+ const severity = code === 'security_pattern_file_too_large' ? 'medium' : 'high';
383
+ pushIssue(issues, `${candidate.relativePath}: ${message}`);
384
+ findings.push(makeFinding(code, severity, candidate.relativePath, message));
385
+ return;
386
+ }
387
+ for (const rule of RULES) {
388
+ if (!rule.appliesTo(candidate, text)) {
389
+ continue;
390
+ }
391
+ for (const match of text.matchAll(rule.pattern)) {
392
+ const line = lineNumberAtIndex(text, match.index ?? 0);
393
+ const matchedLine = text.split(/\r\n|\n|\r/u)[line - 1] ?? '';
394
+ if (rule.linePredicate && !rule.linePredicate(matchedLine, text)) {
395
+ continue;
396
+ }
397
+ addBoundedFinding(findings, issues, policy, makeFinding(rule.code, rule.severity, candidate.relativePath, rule.message, {
398
+ line,
399
+ detector: rule.detector,
400
+ category: rule.category,
401
+ reviewFocus: rule.reviewFocus,
402
+ fingerprint: fingerprint(`${rule.detector}:${match[0]}`),
403
+ }));
404
+ }
405
+ }
406
+ }
407
+ function securityPatternStatus(findings) {
408
+ if (findings.some((finding) => ERROR_CODES.has(finding.code))) {
409
+ return 'error';
410
+ }
411
+ if (findings.some((finding) => ['medium', 'high', 'critical'].includes(finding.severity))) {
412
+ return 'failed';
413
+ }
414
+ return 'passed';
415
+ }
416
+ function summarizeSecurityPatterns(targets, fileCount, findings) {
417
+ const categories = new Set(findings.map((finding) => finding.category).filter((category) => Boolean(category)));
418
+ return {
419
+ target_count: targets.length,
420
+ file_count: fileCount,
421
+ finding_count: findings.length,
422
+ high_or_critical_count: findings.filter((finding) => ['high', 'critical'].includes(finding.severity)).length,
423
+ category_count: categories.size,
424
+ };
425
+ }
426
+ function createInputHash(policy, targets, findings, issues) {
427
+ return sha256Tagged(JSON.stringify({
428
+ policy,
429
+ targets,
430
+ findings: findings.map((finding) => ({
431
+ code: finding.code,
432
+ path: finding.path,
433
+ line: finding.line,
434
+ detector: finding.detector,
435
+ category: finding.category,
436
+ fingerprint: finding.fingerprint,
437
+ })),
438
+ issues,
439
+ }));
440
+ }
441
+ export function inspectSecurityPatternScan(projectRoot, options = {}) {
442
+ const root = path.resolve(projectRoot);
443
+ const policy = {
444
+ max_file_bytes: positiveInteger(options.maxFileBytes, DEFAULT_MAX_FILE_BYTES),
445
+ max_files: positiveInteger(options.maxFiles, DEFAULT_MAX_FILES),
446
+ max_findings: positiveInteger(options.maxFindings, DEFAULT_MAX_FINDINGS),
447
+ extensions: [...SCAN_EXTENSIONS],
448
+ ignored_directories: [...IGNORED_DIRECTORIES],
449
+ evidence_mode: 'metadata_only',
450
+ };
451
+ const targetInputs = options.paths && options.paths.length > 0 ? options.paths : ['.'];
452
+ const targets = [];
453
+ const candidates = new Map();
454
+ const findings = [];
455
+ const issues = [];
456
+ for (const targetPath of targetInputs) {
457
+ let absolutePath;
458
+ let relativePath;
459
+ try {
460
+ const normalized = normalizeTargetPath(root, targetPath);
461
+ absolutePath = normalized.absolutePath;
462
+ relativePath = normalized.relativePath;
463
+ ensureInsideWithoutSymlinks(root, absolutePath, { allowMissingLeaf: true });
464
+ }
465
+ catch (error) {
466
+ const message = error instanceof Error ? error.message : String(error);
467
+ pushIssue(issues, message);
468
+ targets.push({ input: targetPath, path: targetPath, exists: null, kind: 'unknown' });
469
+ findings.push(makeFinding('security_pattern_path_outside_root', 'high', targetPath, message));
470
+ continue;
471
+ }
472
+ let existence;
473
+ try {
474
+ existence = targetKind(absolutePath);
475
+ }
476
+ catch (error) {
477
+ const message = error instanceof Error ? error.message : String(error);
478
+ pushIssue(issues, `${relativePath}: ${message}`);
479
+ targets.push({ input: targetPath, path: relativePath, exists: null, kind: 'unknown' });
480
+ findings.push(makeFinding('security_pattern_unreadable_path', 'high', relativePath, message));
481
+ continue;
482
+ }
483
+ targets.push({ input: targetPath, path: relativePath, exists: existence.exists, kind: existence.kind });
484
+ if (existence.kind === 'file') {
485
+ const surface = surfaceForPath(relativePath);
486
+ if (surface) {
487
+ addCandidate(candidates, findings, issues, policy, { absolutePath, relativePath, surface });
488
+ }
489
+ }
490
+ else if (existence.kind === 'directory') {
491
+ collectFilesFromDirectory(root, absolutePath, candidates, findings, issues, policy);
492
+ }
493
+ }
494
+ for (const candidate of candidates.values()) {
495
+ scanCandidate(root, candidate, policy, findings, issues);
496
+ }
497
+ const status = securityPatternStatus(findings);
498
+ const truncated = findings.some((finding) => ['security_pattern_max_files_exceeded', 'security_pattern_max_findings_exceeded'].includes(finding.code));
499
+ const summary = summarizeSecurityPatterns(targets, candidates.size, findings);
500
+ return {
501
+ schema_version: '1',
502
+ command: 'script-pack',
503
+ pack_id: SECURITY_PATTERN_SCAN_PACK_ID,
504
+ script_id: SECURITY_PATTERN_SCAN_SCRIPT_ID,
505
+ script_ref: SECURITY_PATTERN_SCAN_SCRIPT_REF,
506
+ action: 'scan',
507
+ status,
508
+ ok: status === 'passed',
509
+ mustflow_root: root,
510
+ policy,
511
+ input_hash: createInputHash(policy, targets, findings, issues),
512
+ targets,
513
+ summary,
514
+ truncated,
515
+ findings,
516
+ issues,
517
+ };
518
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.103.10",
3
+ "version": "2.103.12",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
package/schemas/README.md CHANGED
@@ -124,6 +124,10 @@ Current schemas:
124
124
  `mf script-pack run repo/secret-risk-scan scan [path...] --json`, containing plausible
125
125
  hardcoded-secret findings with detector names, paths, line numbers, and redacted fingerprints
126
126
  without printing secret values
127
+ - `security-pattern-scan-report.schema.json`: output of
128
+ `mf script-pack run repo/security-pattern-scan scan [path...] --json`, containing high-signal
129
+ security code-pattern leads with detector names, categories, paths, line numbers, review focus,
130
+ and redacted fingerprints without printing matched source lines or secret values
127
131
  - `text-budget-report.schema.json`: output of
128
132
  `mf script-pack run core/text-budget check <path...> --json`, containing
129
133
  exact text-budget metrics, input content hashes, policy metadata, findings, and JSON Pointer field
@@ -152,6 +156,9 @@ Current schemas:
152
156
  - `repo-approval-gate-report.schema.json`: output of
153
157
  `mf script-pack run repo/approval-gate check --action <type> --json`, containing approval policy
154
158
  decisions, required-action findings, and unreadable policy issues
159
+ - `repo-deploy-surface-report.schema.json`: output of
160
+ `mf script-pack run repo/deploy-surface inspect --json`, containing detected local deploy and
161
+ release surfaces, trigger evidence, required verification, and manual gates
155
162
  - `related-files-report.schema.json`: output of
156
163
  `mf script-pack run repo/related-files map <path...> --json`, containing conservative related-file
157
164
  candidates from direct imports, importers, same-basename siblings, parent configuration files, and