@skillsmith/cli 0.8.9 → 0.8.10

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/dist/cli.js CHANGED
@@ -21005,6 +21005,60 @@ function safePatternMatch(value, pattern) {
21005
21005
  }
21006
21006
  }
21007
21007
 
21008
+ // ../core/dist/src/security/scanner/patterns.sensitive-path.js
21009
+ var ENV_PATH_PATTERN = /\.env(?![A-Za-z])(?!\.(?:example|sample|template|schema|dist))/i;
21010
+ var API_KEY_KEYWORD = /api[_-]?key/i;
21011
+ var AUTH_TOKEN_KEYWORD = /auth[_-]?token/i;
21012
+ var CREDENTIALS_FILE_PATTERN = /credentials\.(?:json|ya?ml|env|toml|txt)/i;
21013
+ var CREDENTIALS_ASSIGN_PATTERN = /credentials\s*[:=]/i;
21014
+ var SECRETS_ASSIGN_PATTERN = /\bsecrets?\s*[:=]/i;
21015
+ var SECRETS_PATH_PATTERN = /\bsecrets?\/[a-z0-9_.-]+/i;
21016
+ var PEM_PATTERN = /\.pem$/i;
21017
+ var KEY_FILE_PATTERN = /\.key$/i;
21018
+ var CRT_PATTERN = /\.crt$/i;
21019
+ var PASSWORD_ASSIGN_PATTERN = /password\s*[:=]/i;
21020
+ var SSH_DIR_PATTERN = /~\/\.ssh/i;
21021
+ var AWS_DIR_PATTERN = /~\/\.aws/i;
21022
+ var CONFIG_DIR_PATTERN = /~\/\.config/i;
21023
+ var ETC_SYSTEM_FILE_PATTERN = /\/etc\/(?:passwd|shadow|sudoers|hosts)\b/i;
21024
+ var SENSITIVE_PATH_PATTERNS = [
21025
+ ENV_PATH_PATTERN,
21026
+ CREDENTIALS_FILE_PATTERN,
21027
+ CREDENTIALS_ASSIGN_PATTERN,
21028
+ SECRETS_ASSIGN_PATTERN,
21029
+ SECRETS_PATH_PATTERN,
21030
+ PEM_PATTERN,
21031
+ KEY_FILE_PATTERN,
21032
+ CRT_PATTERN,
21033
+ PASSWORD_ASSIGN_PATTERN,
21034
+ API_KEY_KEYWORD,
21035
+ AUTH_TOKEN_KEYWORD,
21036
+ SSH_DIR_PATTERN,
21037
+ AWS_DIR_PATTERN,
21038
+ CONFIG_DIR_PATTERN,
21039
+ ETC_SYSTEM_FILE_PATTERN
21040
+ ];
21041
+ var VALUE_GATED_KEYWORD_PATTERNS = /* @__PURE__ */ new Set([
21042
+ API_KEY_KEYWORD,
21043
+ AUTH_TOKEN_KEYWORD
21044
+ ]);
21045
+ var PATH_FORM_PATTERNS = /* @__PURE__ */ new Set([
21046
+ CREDENTIALS_FILE_PATTERN,
21047
+ SECRETS_PATH_PATTERN,
21048
+ PEM_PATTERN,
21049
+ KEY_FILE_PATTERN,
21050
+ CRT_PATTERN,
21051
+ SSH_DIR_PATTERN,
21052
+ AWS_DIR_PATTERN,
21053
+ CONFIG_DIR_PATTERN,
21054
+ ETC_SYSTEM_FILE_PATTERN
21055
+ ]);
21056
+ var VALUE_GATED_ASSIGNMENT_PATTERNS = /* @__PURE__ */ new Set([
21057
+ CREDENTIALS_ASSIGN_PATTERN,
21058
+ SECRETS_ASSIGN_PATTERN,
21059
+ PASSWORD_ASSIGN_PATTERN
21060
+ ]);
21061
+
21008
21062
  // ../core/dist/src/security/scanner/patterns.jailbreak.js
21009
21063
  var JB_IGNORE_PREVIOUS = /ignore\s+(all\s+)?(previous|prior|above)\s+(instructions?|prompts?|rules?)/i;
21010
21064
  var JB_DISREGARD_PREVIOUS = /disregard\s+(all\s+)?(previous|prior|your)\s+(instructions?|programming)/i;
@@ -21290,37 +21344,7 @@ var ANON_PASTE_HOSTS = [
21290
21344
  ];
21291
21345
  var TRANSIENT_TRANSFER_HOSTS = ["transfer.sh", "file.io", "tmpfiles.org", "temp.sh"];
21292
21346
  var URL_SHORTENER_DOMAINS = ["bit.ly", "tinyurl.com", "t.co", "is.gd"];
21293
- var ENV_PATH_PATTERN = /\.env(?![A-Za-z])(?!\.(?:example|sample|template|schema|dist))/i;
21294
- var API_KEY_KEYWORD = /api[_-]?key/i;
21295
- var AUTH_TOKEN_KEYWORD = /auth[_-]?token/i;
21296
- var SENSITIVE_PATH_PATTERNS = [
21297
- ENV_PATH_PATTERN,
21298
- // Contextual credentials: filename or assignment, not bare prose
21299
- /credentials\.(?:json|ya?ml|env|toml|txt)/i,
21300
- /credentials\s*[:=]/i,
21301
- // Contextual secrets: assignment or path, not bare word
21302
- /\bsecrets?\s*[:=]/i,
21303
- /\bsecrets?\/[a-z0-9_.-]+/i,
21304
- /\.pem$/i,
21305
- /\.key$/i,
21306
- /\.crt$/i,
21307
- // Contextual password: assignment or URL (postgres://user:pass@host) only
21308
- /password\s*[:=]/i,
21309
- API_KEY_KEYWORD,
21310
- AUTH_TOKEN_KEYWORD,
21311
- /~\/\.ssh/i,
21312
- /~\/\.aws/i,
21313
- /~\/\.config/i,
21314
- // SMI-4396 Wave 2: explicit system-file paths. Added so that tightening
21315
- // bare /credentials/i and /password/i into assignment-context variants
21316
- // doesn't drop coverage of obvious sensitive references like /etc/passwd.
21317
- /\/etc\/(?:passwd|shadow|sudoers|hosts)\b/i
21318
- ];
21319
- var VALUE_GATED_KEYWORD_PATTERNS = /* @__PURE__ */ new Set([
21320
- API_KEY_KEYWORD,
21321
- AUTH_TOKEN_KEYWORD
21322
- ]);
21323
- var SCANNER_RULESET_VERSION = "2026-08-15.1";
21347
+ var SCANNER_RULESET_VERSION = "2026-09-07.1";
21324
21348
  var SUSPICIOUS_PATTERNS = [
21325
21349
  /eval\s*\(/i,
21326
21350
  /exec\s*\(/i,
@@ -21647,7 +21671,7 @@ var CATEGORY_WEIGHTS = {
21647
21671
 
21648
21672
  // ../core/dist/src/security/scanner/regex-utils.js
21649
21673
  var MAX_LINE_LENGTH_FOR_REGEX = 1e4;
21650
- var MAX_CONTENT_LENGTH_FOR_REGEX = MAX_LINE_LENGTH_FOR_REGEX;
21674
+ var MAX_CONTENT_LENGTH_FOR_REGEX = 1e6;
21651
21675
  function safeRegexTest(pattern, input7, maxLength = MAX_LINE_LENGTH_FOR_REGEX) {
21652
21676
  const safeInput = input7.length > maxLength ? input7.slice(0, maxLength) : input7;
21653
21677
  return safeInput.match(pattern);
@@ -21956,34 +21980,56 @@ function isWithinInlineCode(line, matchIndex) {
21956
21980
  }
21957
21981
  return false;
21958
21982
  }
21983
+ var MAX_MULTILINE_LINES_PER_PATTERN = 64;
21984
+ var MAX_MULTILINE_ITERATIONS_PER_PATTERN = 1e4;
21959
21985
  function scanPatternsWithMultilineSupport(content, config2, lineContexts, maxLength) {
21960
21986
  const lines = content.split("\n");
21961
21987
  const contexts = lineContexts ?? analyzeMarkdownContext(content);
21962
21988
  const bestByLine = /* @__PURE__ */ new Map();
21989
+ let truncated = false;
21963
21990
  const rank = (t) => EVIDENCE_RANK[t];
21991
+ const scannedContent = maxLength !== void 0 && content.length > maxLength ? content.slice(0, maxLength) : content;
21964
21992
  for (const pattern of config2.patterns) {
21965
21993
  if (resolvePatternScope(pattern) === "line")
21966
21994
  continue;
21967
- const match = safeRegexTest(pattern, content, maxLength);
21968
- if (!match)
21969
- continue;
21970
- const matchIndex = match.index ?? content.indexOf(match[0]);
21971
- const lineNumber = content.slice(0, matchIndex).split("\n").length;
21972
- const ctx = contexts[lineNumber - 1];
21973
- const matchLine = lines[lineNumber - 1] ?? "";
21974
- const lineOffset = content.lastIndexOf("\n", matchIndex - 1) + 1;
21975
- const matchCol = matchIndex - lineOffset;
21976
- const inInlineCode = ctx?.isInlineCode && isWithinInlineCode(matchLine, matchCol);
21977
- const inDocContext = ctx ? isDocumentationContext(ctx) || inInlineCode : false;
21978
- const tier = config2.classify(pattern);
21979
- const incumbent = bestByLine.get(lineNumber);
21980
- if (!incumbent || rank(tier) > rank(incumbent.tier)) {
21981
- bestByLine.set(lineNumber, {
21982
- tier,
21983
- matchText: match[0],
21984
- location: match[0].trim().slice(0, 100),
21985
- inDocContext
21986
- });
21995
+ const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
21996
+ const g = new RegExp(pattern.source, flags);
21997
+ const seenLines = /* @__PURE__ */ new Set();
21998
+ let iterations = 0;
21999
+ let m;
22000
+ while ((m = g.exec(scannedContent)) !== null) {
22001
+ if (m[0].length === 0)
22002
+ g.lastIndex++;
22003
+ if (++iterations > MAX_MULTILINE_ITERATIONS_PER_PATTERN) {
22004
+ truncated = true;
22005
+ break;
22006
+ }
22007
+ const matchIndex = m.index;
22008
+ let contentIndex = matchIndex;
22009
+ while (scannedContent[contentIndex] === "\n")
22010
+ contentIndex++;
22011
+ const lineNumber = scannedContent.slice(0, contentIndex).split("\n").length;
22012
+ if (seenLines.has(lineNumber))
22013
+ continue;
22014
+ if (seenLines.size >= MAX_MULTILINE_LINES_PER_PATTERN)
22015
+ break;
22016
+ seenLines.add(lineNumber);
22017
+ const ctx = contexts[lineNumber - 1];
22018
+ const matchLine = lines[lineNumber - 1] ?? "";
22019
+ const lineOffset = scannedContent.lastIndexOf("\n", contentIndex - 1) + 1;
22020
+ const matchCol = contentIndex - lineOffset;
22021
+ const inInlineCode = ctx?.isInlineCode && isWithinInlineCode(matchLine, matchCol);
22022
+ const inDocContext = ctx ? isDocumentationContext(ctx) || inInlineCode : false;
22023
+ const tier = config2.classify(pattern);
22024
+ const incumbent = bestByLine.get(lineNumber);
22025
+ if (!incumbent || rank(tier) > rank(incumbent.tier)) {
22026
+ bestByLine.set(lineNumber, {
22027
+ tier,
22028
+ matchText: m[0],
22029
+ location: m[0].trim().slice(0, 100),
22030
+ inDocContext
22031
+ });
22032
+ }
21987
22033
  }
21988
22034
  }
21989
22035
  lines.forEach((line, index) => {
@@ -22020,11 +22066,11 @@ function scanPatternsWithMultilineSupport(content, config2, lineContexts, maxLen
22020
22066
  if (!candidate)
22021
22067
  continue;
22022
22068
  const { severity, confidence } = resolveEvidenceSeverity(candidate.tier, candidate.inDocContext);
22023
- const truncated = candidate.matchText.slice(0, 50);
22069
+ const truncatedText = candidate.matchText.slice(0, 50);
22024
22070
  findings.push({
22025
22071
  type: config2.type,
22026
22072
  severity,
22027
- message: `${config2.messagePrefix}: "${truncated}${candidate.matchText.length > 50 ? "..." : ""}"`,
22073
+ message: `${config2.messagePrefix}: "${truncatedText}${candidate.matchText.length > 50 ? "..." : ""}"`,
22028
22074
  location: candidate.location,
22029
22075
  lineNumber,
22030
22076
  category: config2.type,
@@ -22033,7 +22079,7 @@ function scanPatternsWithMultilineSupport(content, config2, lineContexts, maxLen
22033
22079
  evidenceType: candidate.tier
22034
22080
  });
22035
22081
  }
22036
- return findings;
22082
+ return { findings, truncated };
22037
22083
  }
22038
22084
 
22039
22085
  // ../core/dist/src/security/scanner/confusables.js
@@ -22629,6 +22675,279 @@ function scanPiiPatterns(content, lineContexts) {
22629
22675
  return findings;
22630
22676
  }
22631
22677
 
22678
+ // ../core/dist/src/security/scanner/SecurityScanner.prose-lexicon.js
22679
+ var NEGATION_TOKENS = /* @__PURE__ */ new Set([
22680
+ "no",
22681
+ "not",
22682
+ "never",
22683
+ "without",
22684
+ "cannot",
22685
+ "can't",
22686
+ "doesn't",
22687
+ "don't",
22688
+ "won't",
22689
+ "didn't",
22690
+ "isn't",
22691
+ "aren't",
22692
+ "nor",
22693
+ "neither"
22694
+ ]);
22695
+ var NOUN_DETERMINERS = /* @__PURE__ */ new Set([
22696
+ "the",
22697
+ "a",
22698
+ "an",
22699
+ "my",
22700
+ "your",
22701
+ "our",
22702
+ "their",
22703
+ "its",
22704
+ "his",
22705
+ "her"
22706
+ ]);
22707
+ var DETECTION_FRAMING = /* @__PURE__ */ new Set([
22708
+ "detect",
22709
+ "detects",
22710
+ "detecting",
22711
+ "detection",
22712
+ "detector",
22713
+ "detectors",
22714
+ "flag",
22715
+ "flags",
22716
+ "flagging",
22717
+ "flagged",
22718
+ "identify",
22719
+ "identifies",
22720
+ "block",
22721
+ "blocks",
22722
+ "blocking",
22723
+ "prevent",
22724
+ "prevents",
22725
+ "warn",
22726
+ "warns",
22727
+ "describe",
22728
+ "describes",
22729
+ "explain",
22730
+ "explains",
22731
+ "scan",
22732
+ "scans",
22733
+ "scanning",
22734
+ "scanner",
22735
+ "audit",
22736
+ "audits",
22737
+ "monitor",
22738
+ "monitors",
22739
+ "catch",
22740
+ "catches",
22741
+ "guard",
22742
+ "guards",
22743
+ "protect",
22744
+ "protects",
22745
+ "rule",
22746
+ "rules",
22747
+ "linter",
22748
+ "analyzer"
22749
+ ]);
22750
+ var RELATIVE_MARKERS = /* @__PURE__ */ new Set(["that", "which", "who", "whom", "whose"]);
22751
+ var PROSE_STOPWORDS = /* @__PURE__ */ new Set([
22752
+ "never",
22753
+ "not",
22754
+ "your",
22755
+ "you",
22756
+ "this",
22757
+ "that",
22758
+ "these",
22759
+ "those",
22760
+ "is",
22761
+ "are",
22762
+ "was",
22763
+ "were",
22764
+ "be",
22765
+ "been",
22766
+ "will",
22767
+ "would",
22768
+ "can",
22769
+ "could",
22770
+ "should",
22771
+ "must",
22772
+ "do",
22773
+ "does",
22774
+ "into",
22775
+ "onto",
22776
+ "from",
22777
+ "with",
22778
+ "without",
22779
+ "by",
22780
+ "for",
22781
+ "of",
22782
+ "to",
22783
+ "in",
22784
+ "on",
22785
+ "at",
22786
+ "as",
22787
+ "and",
22788
+ "or",
22789
+ "but",
22790
+ "if",
22791
+ "when",
22792
+ "where",
22793
+ "how",
22794
+ "why",
22795
+ "use",
22796
+ "using",
22797
+ "used",
22798
+ "see",
22799
+ "via",
22800
+ "per",
22801
+ "they",
22802
+ "them",
22803
+ "their",
22804
+ "it",
22805
+ "its",
22806
+ "we",
22807
+ "our",
22808
+ "us",
22809
+ "please",
22810
+ "ensure",
22811
+ "make",
22812
+ "sure",
22813
+ "the",
22814
+ "a",
22815
+ "an",
22816
+ "all",
22817
+ "any",
22818
+ "each",
22819
+ "no",
22820
+ "nor",
22821
+ "so",
22822
+ "than",
22823
+ "then",
22824
+ "there",
22825
+ "here",
22826
+ "what",
22827
+ "which",
22828
+ "who",
22829
+ "while",
22830
+ "during",
22831
+ "after",
22832
+ "before",
22833
+ "above",
22834
+ "below",
22835
+ "up",
22836
+ "down",
22837
+ "out",
22838
+ "off",
22839
+ "over",
22840
+ "under",
22841
+ "again",
22842
+ "further",
22843
+ "once",
22844
+ "only",
22845
+ "own",
22846
+ "same",
22847
+ "too",
22848
+ "very",
22849
+ "just",
22850
+ "now"
22851
+ ]);
22852
+
22853
+ // ../core/dist/src/security/scanner/SecurityScanner.action-context.js
22854
+ var ACTION_VERBS = "cat|cp|mv|scp|rsync|curl|wget|fetch|tee|tar|zip|gzip|base64|xxd|dd|nc|netcat|chmod|ssh-add|openssl|uploads?|downloads?|sends?|posts?|exfiltrates?|steals?";
22855
+ var ACTION_VERB_EXACT = new RegExp(`^(?:${ACTION_VERBS})$`, "i");
22856
+ var SHELL_OPERATOR = /[|>]/;
22857
+ var NEGATION_LOOKBACK = 3;
22858
+ var NEGATION_LOOKAHEAD = 2;
22859
+ var RELATIVE_CLAUSE_LOOKBACK = 2;
22860
+ function inRelativeClause(norm, i) {
22861
+ for (let j = Math.max(0, i - RELATIVE_CLAUSE_LOOKBACK); j < i; j++) {
22862
+ if (RELATIVE_MARKERS.has(norm[j]))
22863
+ return true;
22864
+ }
22865
+ return false;
22866
+ }
22867
+ function hasActionEvidence(rawLine) {
22868
+ const line = rawLine.slice(0, MAX_LINE_LENGTH_FOR_REGEX);
22869
+ if (SHELL_OPERATOR.test(line))
22870
+ return true;
22871
+ const words = line.split(/\s+/);
22872
+ const norm = words.map((w) => w.toLowerCase().replace(/[^a-z']/g, ""));
22873
+ const describesDetection = norm.some((w) => DETECTION_FRAMING.has(w));
22874
+ for (let i = 0; i < words.length; i++) {
22875
+ if (!ACTION_VERB_EXACT.test(words[i].replace(/[^A-Za-z-]/g, "")))
22876
+ continue;
22877
+ if (i > 0 && NOUN_DETERMINERS.has(norm[i - 1]))
22878
+ continue;
22879
+ if (describesDetection && inRelativeClause(norm, i))
22880
+ continue;
22881
+ let negated = false;
22882
+ for (let j = Math.max(0, i - NEGATION_LOOKBACK); j <= Math.min(norm.length - 1, i + NEGATION_LOOKAHEAD); j++) {
22883
+ if (j !== i && NEGATION_TOKENS.has(norm[j])) {
22884
+ negated = true;
22885
+ break;
22886
+ }
22887
+ }
22888
+ if (!negated)
22889
+ return true;
22890
+ }
22891
+ return false;
22892
+ }
22893
+ var MAX_PATH_ACTION_LINE_DISTANCE = 1;
22894
+ function hasPathActionContext(lines, index) {
22895
+ const lo = Math.max(0, index - MAX_PATH_ACTION_LINE_DISTANCE);
22896
+ const hi = Math.min(lines.length - 1, index + MAX_PATH_ACTION_LINE_DISTANCE);
22897
+ for (let i = lo; i <= hi; i++) {
22898
+ if (hasActionEvidence(lines[i]))
22899
+ return true;
22900
+ }
22901
+ return false;
22902
+ }
22903
+
22904
+ // ../core/dist/src/security/scanner/SecurityScanner.value-gate.js
22905
+ var TEMPLATE_REFERENCE = /^\$|^\{\{|\$\{|^%[A-Za-z_]|^<%/;
22906
+ var MAX_LABEL_TOKENS = 2;
22907
+ var ASSIGNMENT_HEAD = /(?:credentials|\bsecrets?|password)\s*[:=]\s*/gi;
22908
+ function isProseValue(span) {
22909
+ const v = span.replace(/^['"]|['"]$/g, "").trim();
22910
+ if (v.length === 0)
22911
+ return true;
22912
+ if (TEMPLATE_REFERENCE.test(v))
22913
+ return true;
22914
+ if (PLACEHOLDER_SECRET_RE.test(v))
22915
+ return true;
22916
+ if (/^(.)\1+$/.test(v))
22917
+ return true;
22918
+ const tokens = v.split(/\s+/);
22919
+ if (tokens.some((t) => PROSE_STOPWORDS.has(t.toLowerCase().replace(/[^a-z']/g, ""))))
22920
+ return true;
22921
+ if (tokens.length > 12)
22922
+ return true;
22923
+ if (tokens.length > 1 && tokens.length <= MAX_LABEL_TOKENS && tokens.every((t) => /^[a-z]{1,19}$/.test(t)))
22924
+ return true;
22925
+ return false;
22926
+ }
22927
+ function assignmentHasRealValue(lines, index) {
22928
+ const line = lines[index].slice(0, MAX_LINE_LENGTH_FOR_REGEX);
22929
+ const heads = [...line.matchAll(ASSIGNMENT_HEAD)];
22930
+ if (heads.length === 0)
22931
+ return false;
22932
+ let trailingKeyIsBare = false;
22933
+ for (let i = 0; i < heads.length; i++) {
22934
+ const from = (heads[i].index ?? 0) + heads[i][0].length;
22935
+ const to = i + 1 < heads.length ? heads[i + 1].index ?? line.length : line.length;
22936
+ const value = line.slice(from, to);
22937
+ if (value.trim().length === 0) {
22938
+ trailingKeyIsBare = i === heads.length - 1;
22939
+ continue;
22940
+ }
22941
+ trailingKeyIsBare = false;
22942
+ if (!isProseValue(value))
22943
+ return true;
22944
+ }
22945
+ if (!trailingKeyIsBare)
22946
+ return false;
22947
+ const next = lines[index + 1];
22948
+ return next !== void 0 && !isProseValue(next.trim());
22949
+ }
22950
+
22632
22951
  // ../core/dist/src/security/scanner/SecurityScanner.fetch-correlation.js
22633
22952
  var FETCH_COMMAND_PATTERN = /\b(?:curl|wget)\b|\bgit\s+clone\b|\bnpx\b[^\n]{0,80}https?:\/\//i;
22634
22953
  function escapeRegExp2(s) {
@@ -22813,6 +23132,10 @@ function scanSensitivePaths(content, lineContexts) {
22813
23132
  severity = "medium";
22814
23133
  } else if (pattern === ENV_PATH_PATTERN) {
22815
23134
  severity = safeRegexCheck(ENV_EXFIL_CONTEXT, line) ? "high" : "medium";
23135
+ } else if (PATH_FORM_PATTERNS.has(pattern)) {
23136
+ severity = hasPathActionContext(lines, index) ? "high" : "medium";
23137
+ } else if (VALUE_GATED_ASSIGNMENT_PATTERNS.has(pattern)) {
23138
+ severity = assignmentHasRealValue(lines, index) ? "high" : "medium";
22816
23139
  } else {
22817
23140
  severity = "high";
22818
23141
  }
@@ -22820,7 +23143,20 @@ function scanSensitivePaths(content, lineContexts) {
22820
23143
  findings.push({
22821
23144
  type: "sensitive_path",
22822
23145
  severity,
22823
- message: `Reference to potentially sensitive path: ${pattern.source}`,
23146
+ // SMI-5207: the matched TEXT is prepended additively — `pattern.source` is
23147
+ // deliberately retained, not swapped out, because allowlist entries'
23148
+ // `messagePattern` values match on that regex-source substring.
23149
+ // Governance review L-5: `match?.[0] ?? ''` is kept (a plain `match[0]`
23150
+ // fails `tsc` — TS cannot correlate the separate `safeRegexCheck`/
23151
+ // `safeRegexTest` calls the way it narrows a local `if (match)` block, the
23152
+ // pattern this file's OTHER scanners use). Behaviorally the `?? ''`
23153
+ // fallback is unreachable: `safeRegexCheck` (.test()) already gated the
23154
+ // loop above on this same pattern/line pair, and for a non-`g` pattern
23155
+ // .test() and .match() can never disagree on the same input. If it ever
23156
+ // WAS reached, it would silently diverge from the edge twins (which emit
23157
+ // no finding at all rather than one with an empty quoted span) — a gap the
23158
+ // twin-parity test can't catch, since it compares files, not behavior.
23159
+ message: `Reference to potentially sensitive path: "${match?.[0]?.slice(0, 60) ?? ""}" (${pattern.source})`,
22824
23160
  location: line.trim().slice(0, 100),
22825
23161
  lineNumber: index + 1,
22826
23162
  inDocumentationContext: inDocContext,
@@ -23485,6 +23821,7 @@ function scanDecoyMisdirection(content, lineContexts) {
23485
23821
  }
23486
23822
 
23487
23823
  // ../core/dist/src/security/scanner/SecurityScanner.js
23824
+ var DIRECTIVE_JAILBREAK_PATTERNS = JAILBREAK_PATTERNS.filter((p) => classifyEvidence(p) !== "mention");
23488
23825
  var SecurityScanner = class {
23489
23826
  allowedDomains;
23490
23827
  blockedPatterns;
@@ -23603,13 +23940,26 @@ var SecurityScanner = class {
23603
23940
  * text contains. This disables ONLY the encoded-payload detector on the
23604
23941
  * inner call, not the rest of the suite — a decoded `curl|bash` still
23605
23942
  * trips `code_execution`, decoded secrets still trip `sensitive_path`, etc.
23606
- */
23607
- runDetectors(content, lineContexts, skipEncodedPayload, isHighTrustAuthor = false, isMarkdown = true) {
23943
+ *
23944
+ * SMI-5879: `EncodedPayloadRescanner`'s contract is a bare
23945
+ * `SecurityFinding[]` return (SecurityScanner.encoding.ts, out of scope for
23946
+ * this change), so a recursive `runDetectors` call for decoded content has
23947
+ * no return channel of its own to report multiline truncation.
23948
+ * `truncationRef` is a shared mutable out-param instead: every call (outer
23949
+ * and any recursive rescan) ORs its own jailbreak/AI-defence truncation
23950
+ * into it, so truncation anywhere in the recursion is visible to the
23951
+ * original (outermost) caller. Per `ScanReport.multilineTruncated`'s own
23952
+ * doc: being truncated is only ever used to RAISE caution, never to lower
23953
+ * it, so over-reporting here (e.g. attributing an inner rescan's
23954
+ * truncation to the whole document) is safe by that same design.
23955
+ */
23956
+ runDetectors(content, lineContexts, skipEncodedPayload, isHighTrustAuthor = false, isMarkdown = true, truncationRef) {
23608
23957
  const findings = [];
23609
23958
  const effectiveMultilineLimit = Math.min(MAX_CONTENT_LENGTH_FOR_REGEX, this.maxContentLength);
23610
23959
  findings.push(...this.scanUrls(content));
23611
23960
  findings.push(...scanSensitivePaths(content, lineContexts));
23612
- findings.push(...this.scanJailbreakPatterns(content, lineContexts, effectiveMultilineLimit));
23961
+ const jailbreakResult = this.scanJailbreakPatterns(content, lineContexts, effectiveMultilineLimit);
23962
+ findings.push(...jailbreakResult.findings);
23613
23963
  findings.push(...this.scanSuspiciousPatterns(content, lineContexts));
23614
23964
  findings.push(...scanSocialEngineering(content, lineContexts));
23615
23965
  findings.push(...scanPromptLeaking(content, lineContexts));
@@ -23621,7 +23971,11 @@ var SecurityScanner = class {
23621
23971
  findings.push(...scanArchiveEvasion(content, lineContexts));
23622
23972
  findings.push(...scanPasteHostFetch(content, lineContexts));
23623
23973
  findings.push(...scanDecoyMisdirection(content, lineContexts));
23624
- findings.push(...this.scanAIDefenceVulnerabilities(content, lineContexts, effectiveMultilineLimit));
23974
+ const aiDefenceResult = this.scanAIDefenceVulnerabilities(content, lineContexts, effectiveMultilineLimit);
23975
+ findings.push(...aiDefenceResult.findings);
23976
+ if (truncationRef) {
23977
+ truncationRef.truncated = truncationRef.truncated || jailbreakResult.truncated || aiDefenceResult.truncated;
23978
+ }
23625
23979
  findings.push(...scanSsrfPatterns(content, lineContexts, effectiveMultilineLimit));
23626
23980
  findings.push(...scanPiiPatterns(content, lineContexts));
23627
23981
  findings.push(...scanCodeExecution(content, lineContexts));
@@ -23629,7 +23983,7 @@ var SecurityScanner = class {
23629
23983
  escalateCodeExecution(findings);
23630
23984
  escalateCorroboratedMentions(findings);
23631
23985
  if (!skipEncodedPayload) {
23632
- findings.push(...scanEncodedPayload(content, lineContexts, (decodedContent) => this.runDetectors(decodedContent, analyzeMarkdownContext(decodedContent, isMarkdown), true, isHighTrustAuthor, isMarkdown)));
23986
+ findings.push(...scanEncodedPayload(content, lineContexts, (decodedContent) => this.runDetectors(decodedContent, analyzeMarkdownContext(decodedContent, isMarkdown), true, isHighTrustAuthor, isMarkdown, truncationRef)));
23633
23987
  }
23634
23988
  return findings;
23635
23989
  }
@@ -23671,7 +24025,8 @@ var SecurityScanner = class {
23671
24025
  message: `Multiline regex scan truncated at ${effectiveMultilineLimit} code units (content is ${content.length} code units; configured maxContentLength is ${this.maxContentLength} code units)`
23672
24026
  });
23673
24027
  }
23674
- findings.push(...this.runDetectors(content, lineContexts, false, isHighTrustAuthor, isMarkdown));
24028
+ const truncationRef = { truncated: false };
24029
+ findings.push(...this.runDetectors(content, lineContexts, false, isHighTrustAuthor, isMarkdown, truncationRef));
23675
24030
  const endTime = performance.now();
23676
24031
  const { total: riskScore, breakdown: riskBreakdown } = calculateRiskScore(findings);
23677
24032
  const hasCritical = findings.some((f) => f.severity === "critical");
@@ -23684,11 +24039,17 @@ var SecurityScanner = class {
23684
24039
  scannedAt: /* @__PURE__ */ new Date(),
23685
24040
  scanDurationMs: endTime - startTime,
23686
24041
  riskScore,
23687
- riskBreakdown
24042
+ riskBreakdown,
24043
+ multilineTruncated: truncationRef.truncated
23688
24044
  };
23689
24045
  }
24046
+ /**
24047
+ * SMI-5879 (design §5): tests only the directive-tier derived subset (a
24048
+ * bare mention like "jailbreak" or "DAN" alone should not fail the quick
24049
+ * path — see DIRECTIVE_JAILBREAK_PATTERNS above).
24050
+ */
23690
24051
  quickCheck(content) {
23691
- for (const pattern of JAILBREAK_PATTERNS) {
24052
+ for (const pattern of DIRECTIVE_JAILBREAK_PATTERNS) {
23692
24053
  if (safeRegexCheck(pattern, content))
23693
24054
  return false;
23694
24055
  }
@@ -35163,6 +35524,17 @@ function checkCursorMcpArtifact(path30) {
35163
35524
  };
35164
35525
  }
35165
35526
 
35527
+ // ../core/dist/src/utils/skill-name.js
35528
+ var VALID_SKILL_NAME_RE = /^[a-z][a-z0-9-]*$/;
35529
+ function validateSkillName(name) {
35530
+ if (!name.trim())
35531
+ return "Skill name is required";
35532
+ if (!VALID_SKILL_NAME_RE.test(name)) {
35533
+ return "Skill name must start with a lowercase letter and contain only lowercase letters, digits, and hyphens (e.g. my-skill)";
35534
+ }
35535
+ return true;
35536
+ }
35537
+
35166
35538
  // ../core/dist/src/provenance/git-config.js
35167
35539
  import * as fs8 from "fs";
35168
35540
  import * as path10 from "path";
@@ -37758,16 +38130,6 @@ import { mkdir as mkdir9, writeFile as writeFile6, readFile as readFile12, stat
37758
38130
  import { dirname as dirname22, join as join51, resolve as resolve15 } from "path";
37759
38131
  import { createHash as createHash10 } from "crypto";
37760
38132
 
37761
- // src/utils/skill-name.ts
37762
- var VALID_SKILL_NAME_RE = /^[a-z][a-z0-9-]*$/;
37763
- function validateSkillName(name) {
37764
- if (!name.trim()) return "Skill name is required";
37765
- if (!VALID_SKILL_NAME_RE.test(name)) {
37766
- return "Skill name must start with a lowercase letter and contain only lowercase letters, digits, and hyphens (e.g. my-skill)";
37767
- }
37768
- return true;
37769
- }
37770
-
37771
38133
  // src/commands/author/utils.ts
37772
38134
  import { access as access3 } from "fs/promises";
37773
38135
  import { mkdir as mkdir7 } from "fs/promises";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillsmith/cli",
3
- "version": "0.8.9",
3
+ "version": "0.8.10",
4
4
  "description": "A registry for sharing, scanning, and tracking agent skills across teams.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,14 +39,14 @@
39
39
  "yaml": "2.8.3"
40
40
  },
41
41
  "devDependencies": {
42
- "@skillsmith/core": "^0.12.2",
43
- "@skillsmith/mcp-server": "^0.7.13",
42
+ "@skillsmith/core": "^0.12.3",
43
+ "@skillsmith/mcp-server": "^0.7.14",
44
44
  "@types/node": "^25.9.3",
45
45
  "esbuild": "0.28.1",
46
46
  "vitest": "4.1.11"
47
47
  },
48
48
  "peerDependencies": {
49
- "@smith-horn/enterprise": "^0.3.9"
49
+ "@smith-horn/enterprise": "^0.3.10"
50
50
  },
51
51
  "peerDependenciesMeta": {
52
52
  "@smith-horn/enterprise": {