@promptshield/core
Advanced tools
+271
-74
@@ -82,3 +82,3 @@ /** | ||
| */ | ||
| interface ThreatLoc { | ||
| interface Location { | ||
| /** 1-based line number */ | ||
@@ -98,3 +98,3 @@ line: number; | ||
| */ | ||
| interface ThreatReport { | ||
| interface ThreatReportWithoutLocation { | ||
| /** | ||
@@ -119,3 +119,8 @@ * Stable rule identifier. | ||
| /** Location of the threat start */ | ||
| loc: ThreatLoc; | ||
| range: { | ||
| /** Offset: 0-based character index */ | ||
| start: number; | ||
| /** Offset: 0-based character index */ | ||
| end: number; | ||
| }; | ||
| /** | ||
@@ -155,9 +160,63 @@ * The substring responsible for the detection. | ||
| referenceUrl: string; | ||
| /** | ||
| * Indicates whether this threat was suppressed | ||
| * by an ignore directive. | ||
| */ | ||
| suppressed?: boolean; | ||
| } | ||
| /** | ||
| * Threat report enriched with human-readable location information. | ||
| * | ||
| * `ThreatReport` extends the base `ThreatReportWithoutLocation` by replacing the | ||
| * offset-based `range` with resolved line/column locations. This format is | ||
| * intended for environments where diagnostics must be presented to humans, | ||
| * such as: | ||
| * | ||
| * - CLI output | ||
| * - CI reports | ||
| * - logs | ||
| * - editor diagnostics | ||
| * | ||
| * The core scanner operates purely on absolute character offsets for | ||
| * performance and interoperability with editor APIs (e.g., Tiptap, LSP). | ||
| * Location resolution is performed later using utilities such as | ||
| * `enrichWithLoc`. | ||
| * | ||
| * Each range endpoint includes: | ||
| * | ||
| * - `line` — 1-based line number | ||
| * - `column` — 1-based column number | ||
| * - `index` — original 0-based character offset | ||
| * | ||
| * Keeping the original `index` ensures deterministic mapping back to the | ||
| * source text while still providing user-friendly diagnostics. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * { | ||
| * ruleId: "PSU001", | ||
| * severity: "LOW", | ||
| * message: "Invisible Unicode characters detected.", | ||
| * range: { | ||
| * start: { line: 2, column: 5, index: 17 }, | ||
| * end: { line: 2, column: 6, index: 18 } | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| interface ThreatReport extends Omit<ThreatReportWithoutLocation, "range"> { | ||
| range: { | ||
| /** Start position of the detected threat span. */ | ||
| start: Location; | ||
| /** End position of the detected threat span. */ | ||
| end: Location; | ||
| }; | ||
| } | ||
| /** | ||
| * Predicate used by the scanner to determine whether a detected | ||
| * threat span should be ignored. | ||
| * | ||
| * @param start - Absolute 0-based character index of the threat start (inclusive). | ||
| * @param end - Absolute 0-based character index of the threat end (exclusive). | ||
| * | ||
| * The predicate should return `true` only if the **entire span** | ||
| * falls within an ignore range. | ||
| */ | ||
| type IgnoreChecker = (start: number, end: number) => boolean; | ||
| /** | ||
| * Scanner configuration options. | ||
@@ -210,2 +269,8 @@ */ | ||
| disableInjectionPatterns?: boolean; | ||
| /** | ||
| * Ignore checker function. | ||
| * | ||
| * @default () => false | ||
| */ | ||
| ignoreChecker?: IgnoreChecker; | ||
| } | ||
@@ -231,3 +296,3 @@ /** | ||
| */ | ||
| lineOffsets?: number[]; | ||
| lineOffsets: number[]; | ||
| } | ||
@@ -242,11 +307,4 @@ /** | ||
| */ | ||
| type Detector = (text: string, options: ScanOptions, context: ScanContext) => ThreatReport[]; | ||
| type Detector = (text: string, options: ScanOptions) => ThreatReportWithoutLocation[]; | ||
| /** | ||
| * Performance metrics for a scan. | ||
| */ | ||
| interface ScanStats { | ||
| durationMs: number; | ||
| totalChars: number; | ||
| } | ||
| /** | ||
| * Result returned by the scanner. | ||
@@ -256,3 +314,2 @@ */ | ||
| threats: ThreatReport[]; | ||
| stats: ScanStats; | ||
| isClean: boolean; | ||
@@ -262,2 +319,25 @@ } | ||
| /** | ||
| * Core scanning entry point. | ||
| * | ||
| * Executes all enabled detectors in priority order: | ||
| * | ||
| * 1. Trojan Source (BIDI logic manipulation) | ||
| * 2. Invisible characters | ||
| * 3. Homoglyph spoofing | ||
| * 4. Unicode normalization anomalies | ||
| * 5. Smuggling techniques | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { scan } from '@promptshield/core'; | ||
| * | ||
| * const result = scan("Hello\u200BWorld"); | ||
| * if (!result.isClean) { | ||
| * console.log(result.threats); | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare const scan: (text: string, options?: ScanOptions) => ScanResult; | ||
| /** | ||
| * Homoglyph detector. | ||
@@ -292,21 +372,45 @@ * | ||
| */ | ||
| declare const scanHomoglyphs: (text: string, options?: ScanOptions, context?: ScanContext) => ThreatReport[]; | ||
| declare const scanHomoglyphs: (text: string, options?: ScanOptions) => ThreatReportWithoutLocation[]; | ||
| /** | ||
| * Scan for deterministic prompt-injection patterns. | ||
| * Scan text for deterministic prompt-injection patterns. | ||
| * | ||
| * Detection strategy: | ||
| * - Scan line-by-line for stable location reporting | ||
| * - Attempt direct regex detection first | ||
| * - Fall back to normalized detection | ||
| * | ||
| * Span semantics: | ||
| * offendingText = matched instruction phrase or entire line | ||
| * 1. Perform **direct regex matching** against the raw text. | ||
| * 2. Perform **normalized matching** to catch obfuscation such as: | ||
| * | ||
| * - excessive whitespace | ||
| * - character splitting | ||
| * - accent obfuscation | ||
| * | ||
| * Example: | ||
| * | ||
| * i g n o r e | ||
| * previous | ||
| * instructions | ||
| * | ||
| * To avoid duplicate reporting: | ||
| * | ||
| * - Direct matches are recorded first. | ||
| * - Normalized matches are skipped if they overlap an already | ||
| * detected span for the same rule. | ||
| * | ||
| * Complexity: | ||
| * | ||
| * - One normalization pass over the text | ||
| * - One regex scan per rule | ||
| * - One incremental normalized search per rule | ||
| * | ||
| * Overall runtime remains **linear in input size**. | ||
| * | ||
| * @param text Raw text to scan | ||
| * @param options Scanner configuration | ||
| */ | ||
| declare const scanInjectionPatterns: (text: string, options?: ScanOptions, context?: ScanContext) => ThreatReport[]; | ||
| declare const scanInjectionPatterns: (text: string, options?: ScanOptions) => ThreatReportWithoutLocation[]; | ||
| /** | ||
| * Invisible-character detector. | ||
| * Invisible character detector. | ||
| * | ||
| * Emits one primary span-level rule using precedence: | ||
| * This detector emits **span-level threats** with the following precedence: | ||
| * | ||
@@ -317,5 +421,13 @@ * PSU004 → Unicode tag payload | ||
| * | ||
| * PSU002 is emitted independently for boundary manipulation. | ||
| * Additionally: | ||
| * | ||
| * PSU002 is emitted independently for **token boundary manipulation** | ||
| * where an invisible character appears inside a visible token. | ||
| * | ||
| * Span semantics: | ||
| * | ||
| * • offendingText represents the **entire invisible sequence** | ||
| * • spans are **not merged across newline boundaries** | ||
| */ | ||
| declare const scanInvisibleChars: (text: string, options?: ScanOptions, context?: ScanContext) => ThreatReport[]; | ||
| declare const scanInvisibleChars: (text: string, options?: ScanOptions) => ThreatReportWithoutLocation[]; | ||
| /** | ||
@@ -330,6 +442,6 @@ * Attempts to decode Unicode tag characters into ASCII text. | ||
| * | ||
| * ASCII = codePoint - 0xE0000 | ||
| * ASCII = codePoint − 0xE0000 | ||
| * | ||
| * Attackers can use this mechanism to embed hidden instructions | ||
| * or metadata inside otherwise invisible text streams. | ||
| * This mechanism has been abused in multiple security reports to embed | ||
| * hidden instructions inside invisible character streams. | ||
| * | ||
@@ -343,31 +455,42 @@ * This decoder performs a best-effort extraction. | ||
| * | ||
| * Detects characters that change under NFKC normalization. | ||
| * Detects characters whose representation changes under **NFKC normalization**. | ||
| * | ||
| * Unicode normalization can transform visually similar characters | ||
| * into canonical equivalents. When user-visible text differs from | ||
| * its normalized form, this may indicate: | ||
| * Unicode normalization may transform visually similar or compatibility | ||
| * characters into canonical equivalents. When displayed text differs from | ||
| * its normalized form, this can introduce ambiguity between what users see | ||
| * and what downstream systems interpret. | ||
| * | ||
| * Such situations may indicate: | ||
| * | ||
| * - compatibility glyph usage | ||
| * - spoofing attempts | ||
| * - homoglyph confusion | ||
| * - prompt-smuggling techniques | ||
| * - content-validation bypass | ||
| * - prompt smuggling techniques | ||
| * - validation bypass in downstream processing pipelines | ||
| * | ||
| * Detection model: | ||
| * - Compare each character against its NFKC-normalized form | ||
| * - Group adjacent normalization-sensitive characters into a span | ||
| * - Emit one threat per span | ||
| * | ||
| * NOTE: | ||
| * This is intentionally heuristic. Many normalization changes are | ||
| * legitimate in multilingual text, but normalization-sensitive spans | ||
| * in prompts or code should be surfaced for inspection. | ||
| * 1. Normalize the text using **NFKC** | ||
| * 2. Iterate over characters in the original text | ||
| * 3. Identify characters whose normalized form differs | ||
| * 4. Group adjacent normalization-sensitive characters into spans | ||
| * 5. Emit one threat per span | ||
| * | ||
| * Severity heuristic: | ||
| * | ||
| * - **PSN001 (LOW)** | ||
| * Compatibility normalization producing simple ASCII text. | ||
| * | ||
| * - **PSN002 (MEDIUM)** | ||
| * More complex normalization transformations. | ||
| * | ||
| * Span semantics: | ||
| * offendingText = original span | ||
| * decodedPayload = normalized span | ||
| * | ||
| * Rule: | ||
| * PSN001 — Normalization-sensitive text detected | ||
| * offendingText = original span | ||
| * decodedPayload = normalized span | ||
| * | ||
| * Normalization can expand characters (example: `ff → ff`), therefore | ||
| * the normalized payload is computed from the entire span. | ||
| */ | ||
| declare const scanNormalization: (text: string, options?: ScanOptions, context?: ScanContext) => ThreatReport[]; | ||
| declare const scanNormalization: (text: string, options?: ScanOptions) => ThreatReportWithoutLocation[]; | ||
@@ -379,3 +502,4 @@ /** biome-ignore-all lint/suspicious/noAssignInExpressions: iterating over regex matches */ | ||
| * | ||
| * Detects techniques used to conceal instructions or data inside text. | ||
| * Detects techniques used to conceal instructions or payloads | ||
| * inside otherwise harmless-looking text. | ||
| * | ||
@@ -386,12 +510,13 @@ * Rules emitted: | ||
| * PSS002 — Base64 payload with readable content (MEDIUM) | ||
| * PSS006 — Hex-encoded payload with readable content (MEDIUM) | ||
| * PSS003 — Hidden Markdown comment (LOW) | ||
| * PSS004 — Invisible Markdown link (LOW) | ||
| * PSS005 — Hidden HTML container (LOW) | ||
| * | ||
| * Span semantics: | ||
| * offendingText = entire suspicious region | ||
| * decodedPayload = recovered payload when available | ||
| * | ||
| * Context is intentionally mutable so detectors can share `lineOffsets`. | ||
| * offendingText = suspicious region | ||
| * decodedPayload = recovered payload when available | ||
| */ | ||
| declare const scanSmuggling: (text: string, options?: ScanOptions, context?: ScanContext) => ThreatReport[]; | ||
| declare const scanSmuggling: (text: string, options?: ScanOptions) => ThreatReportWithoutLocation[]; | ||
@@ -402,32 +527,104 @@ /** | ||
| * Detects unsafe usage of Unicode Bidirectional (BIDI) control characters | ||
| * that may cause visual ordering to differ from logical ordering. | ||
| * that can cause the *visual order* of text to differ from its *logical order*. | ||
| * | ||
| * These attacks allow malicious code or instructions to appear benign to | ||
| * reviewers while executing differently when interpreted by compilers, | ||
| * interpreters, or LLMs. | ||
| * | ||
| * Detection rules: | ||
| * | ||
| * PST001 — Matched BIDI override sequence | ||
| * PST002 — Unterminated BIDI override sequence | ||
| */ | ||
| declare const scanTrojanSource: (text: string, options?: ScanOptions, context?: ScanContext) => ThreatReport[]; | ||
| declare const scanTrojanSource: (text: string, options?: ScanOptions) => ThreatReportWithoutLocation[]; | ||
| /** | ||
| * Core scanning entry point. | ||
| * Computes line start offsets for a string. | ||
| * | ||
| * Executes all enabled detectors in priority order: | ||
| * Each entry represents the character index where a new line begins. | ||
| * The first entry is always `0`. | ||
| * | ||
| * 1. Trojan Source (BIDI logic manipulation) | ||
| * 2. Invisible characters | ||
| * 3. Homoglyph spoofing | ||
| * 4. Unicode normalization anomalies | ||
| * 5. Smuggling techniques | ||
| * Example: | ||
| * "a\nb\nc" → [0, 2, 4] | ||
| * | ||
| * The provided `context` object is shared across detectors and may be | ||
| * mutated for performance optimizations (e.g., caching line offsets). | ||
| * This enables fast index → (line, column) mapping without repeatedly | ||
| * scanning the entire string. | ||
| */ | ||
| declare const getLineOffsets: (text: string) => number[]; | ||
| /** | ||
| * Resolves a character index into a line/column location. | ||
| * | ||
| * Uses binary search over precomputed line offsets for O(log n) lookup. | ||
| * | ||
| * Context provides: | ||
| * - baseLine | ||
| * - baseCol | ||
| * - lineOffsets | ||
| * | ||
| * `baseLine` and `baseCol` allow this function to operate correctly when | ||
| * scanning substrings that originate from a larger document. | ||
| */ | ||
| declare const getLocForIndex: (index: number, context: Required<ScanContext>) => { | ||
| line: number; | ||
| column: number; | ||
| index: number; | ||
| }; | ||
| /** | ||
| * Enriches ThreatReports with human-readable line/column locations. | ||
| * | ||
| * PromptShield detectors operate on absolute character offsets for | ||
| * performance and editor compatibility (e.g., Tiptap, LSP, AST tools). | ||
| * | ||
| * However, human-facing environments such as: | ||
| * | ||
| * - CLI output | ||
| * - CI diagnostics | ||
| * - logs | ||
| * - static analysis reports | ||
| * | ||
| * require line and column information. | ||
| * | ||
| * This helper converts offset-based threat ranges into location-aware | ||
| * structures in a single pass. | ||
| * | ||
| * The function computes line offsets once and resolves both start and | ||
| * end positions using binary search (`getLocForIndex`). | ||
| * | ||
| * This approach is significantly more efficient than resolving locations | ||
| * during detection or performing repeated scans of the input text. | ||
| * | ||
| * @param threats | ||
| * List of ThreatReports produced by `scan()`. These must contain | ||
| * offset-based ranges (`range.start`, `range.end`). | ||
| * | ||
| * @param text | ||
| * The original scanned text used to generate the threats. | ||
| * | ||
| * @param context | ||
| * Optional scan context for offset translation. Supports: | ||
| * - `baseLine` | ||
| * - `baseCol` | ||
| * | ||
| * This is useful when scanning substrings embedded inside a larger | ||
| * document (e.g., editor buffers, LSP fragments). | ||
| * | ||
| * @returns | ||
| * A new array of ThreatReports where each threat includes | ||
| * `loc.start` and `loc.end` describing the resolved line/column | ||
| * positions. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { scan } from '@promptshield/core'; | ||
| * const result = scan(text); | ||
| * const threats = enrichWithLoc(result.threats, text); | ||
| * | ||
| * const result = scan("Hello\u200BWorld"); | ||
| * if (!result.isClean) { | ||
| * console.log(result.threats); | ||
| * } | ||
| * console.log(threats[0].loc); | ||
| * // { | ||
| * // start: { line: 2, column: 5, index: 17 }, | ||
| * // end: { line: 2, column: 8, index: 20 } | ||
| * // } | ||
| * ``` | ||
| */ | ||
| declare const scan: (text: string, options?: ScanOptions, context?: ScanContext) => ScanResult; | ||
| declare const enrichWithLocation: (threats: ThreatReportWithoutLocation[], text: string, context?: Omit<ScanContext, "lineOffsets">) => ThreatReport[]; | ||
| export { type Detector, SEVERITY_MAP, type ScanContext, type ScanOptions, type ScanResult, type ScanStats, type Severity, ThreatCategory, type ThreatLoc, type ThreatReport, decodeUnicodeTags, scan, scanHomoglyphs, scanInjectionPatterns, scanInvisibleChars, scanNormalization, scanSmuggling, scanTrojanSource }; | ||
| export { type Detector, type IgnoreChecker, type Location, SEVERITY_MAP, type ScanContext, type ScanOptions, type ScanResult, type Severity, ThreatCategory, type ThreatReport, type ThreatReportWithoutLocation, decodeUnicodeTags, enrichWithLocation, getLineOffsets, getLocForIndex, scan, scanHomoglyphs, scanInjectionPatterns, scanInvisibleChars, scanNormalization, scanSmuggling, scanTrojanSource }; |
+271
-74
@@ -82,3 +82,3 @@ /** | ||
| */ | ||
| interface ThreatLoc { | ||
| interface Location { | ||
| /** 1-based line number */ | ||
@@ -98,3 +98,3 @@ line: number; | ||
| */ | ||
| interface ThreatReport { | ||
| interface ThreatReportWithoutLocation { | ||
| /** | ||
@@ -119,3 +119,8 @@ * Stable rule identifier. | ||
| /** Location of the threat start */ | ||
| loc: ThreatLoc; | ||
| range: { | ||
| /** Offset: 0-based character index */ | ||
| start: number; | ||
| /** Offset: 0-based character index */ | ||
| end: number; | ||
| }; | ||
| /** | ||
@@ -155,9 +160,63 @@ * The substring responsible for the detection. | ||
| referenceUrl: string; | ||
| /** | ||
| * Indicates whether this threat was suppressed | ||
| * by an ignore directive. | ||
| */ | ||
| suppressed?: boolean; | ||
| } | ||
| /** | ||
| * Threat report enriched with human-readable location information. | ||
| * | ||
| * `ThreatReport` extends the base `ThreatReportWithoutLocation` by replacing the | ||
| * offset-based `range` with resolved line/column locations. This format is | ||
| * intended for environments where diagnostics must be presented to humans, | ||
| * such as: | ||
| * | ||
| * - CLI output | ||
| * - CI reports | ||
| * - logs | ||
| * - editor diagnostics | ||
| * | ||
| * The core scanner operates purely on absolute character offsets for | ||
| * performance and interoperability with editor APIs (e.g., Tiptap, LSP). | ||
| * Location resolution is performed later using utilities such as | ||
| * `enrichWithLoc`. | ||
| * | ||
| * Each range endpoint includes: | ||
| * | ||
| * - `line` — 1-based line number | ||
| * - `column` — 1-based column number | ||
| * - `index` — original 0-based character offset | ||
| * | ||
| * Keeping the original `index` ensures deterministic mapping back to the | ||
| * source text while still providing user-friendly diagnostics. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * { | ||
| * ruleId: "PSU001", | ||
| * severity: "LOW", | ||
| * message: "Invisible Unicode characters detected.", | ||
| * range: { | ||
| * start: { line: 2, column: 5, index: 17 }, | ||
| * end: { line: 2, column: 6, index: 18 } | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| interface ThreatReport extends Omit<ThreatReportWithoutLocation, "range"> { | ||
| range: { | ||
| /** Start position of the detected threat span. */ | ||
| start: Location; | ||
| /** End position of the detected threat span. */ | ||
| end: Location; | ||
| }; | ||
| } | ||
| /** | ||
| * Predicate used by the scanner to determine whether a detected | ||
| * threat span should be ignored. | ||
| * | ||
| * @param start - Absolute 0-based character index of the threat start (inclusive). | ||
| * @param end - Absolute 0-based character index of the threat end (exclusive). | ||
| * | ||
| * The predicate should return `true` only if the **entire span** | ||
| * falls within an ignore range. | ||
| */ | ||
| type IgnoreChecker = (start: number, end: number) => boolean; | ||
| /** | ||
| * Scanner configuration options. | ||
@@ -210,2 +269,8 @@ */ | ||
| disableInjectionPatterns?: boolean; | ||
| /** | ||
| * Ignore checker function. | ||
| * | ||
| * @default () => false | ||
| */ | ||
| ignoreChecker?: IgnoreChecker; | ||
| } | ||
@@ -231,3 +296,3 @@ /** | ||
| */ | ||
| lineOffsets?: number[]; | ||
| lineOffsets: number[]; | ||
| } | ||
@@ -242,11 +307,4 @@ /** | ||
| */ | ||
| type Detector = (text: string, options: ScanOptions, context: ScanContext) => ThreatReport[]; | ||
| type Detector = (text: string, options: ScanOptions) => ThreatReportWithoutLocation[]; | ||
| /** | ||
| * Performance metrics for a scan. | ||
| */ | ||
| interface ScanStats { | ||
| durationMs: number; | ||
| totalChars: number; | ||
| } | ||
| /** | ||
| * Result returned by the scanner. | ||
@@ -256,3 +314,2 @@ */ | ||
| threats: ThreatReport[]; | ||
| stats: ScanStats; | ||
| isClean: boolean; | ||
@@ -262,2 +319,25 @@ } | ||
| /** | ||
| * Core scanning entry point. | ||
| * | ||
| * Executes all enabled detectors in priority order: | ||
| * | ||
| * 1. Trojan Source (BIDI logic manipulation) | ||
| * 2. Invisible characters | ||
| * 3. Homoglyph spoofing | ||
| * 4. Unicode normalization anomalies | ||
| * 5. Smuggling techniques | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { scan } from '@promptshield/core'; | ||
| * | ||
| * const result = scan("Hello\u200BWorld"); | ||
| * if (!result.isClean) { | ||
| * console.log(result.threats); | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare const scan: (text: string, options?: ScanOptions) => ScanResult; | ||
| /** | ||
| * Homoglyph detector. | ||
@@ -292,21 +372,45 @@ * | ||
| */ | ||
| declare const scanHomoglyphs: (text: string, options?: ScanOptions, context?: ScanContext) => ThreatReport[]; | ||
| declare const scanHomoglyphs: (text: string, options?: ScanOptions) => ThreatReportWithoutLocation[]; | ||
| /** | ||
| * Scan for deterministic prompt-injection patterns. | ||
| * Scan text for deterministic prompt-injection patterns. | ||
| * | ||
| * Detection strategy: | ||
| * - Scan line-by-line for stable location reporting | ||
| * - Attempt direct regex detection first | ||
| * - Fall back to normalized detection | ||
| * | ||
| * Span semantics: | ||
| * offendingText = matched instruction phrase or entire line | ||
| * 1. Perform **direct regex matching** against the raw text. | ||
| * 2. Perform **normalized matching** to catch obfuscation such as: | ||
| * | ||
| * - excessive whitespace | ||
| * - character splitting | ||
| * - accent obfuscation | ||
| * | ||
| * Example: | ||
| * | ||
| * i g n o r e | ||
| * previous | ||
| * instructions | ||
| * | ||
| * To avoid duplicate reporting: | ||
| * | ||
| * - Direct matches are recorded first. | ||
| * - Normalized matches are skipped if they overlap an already | ||
| * detected span for the same rule. | ||
| * | ||
| * Complexity: | ||
| * | ||
| * - One normalization pass over the text | ||
| * - One regex scan per rule | ||
| * - One incremental normalized search per rule | ||
| * | ||
| * Overall runtime remains **linear in input size**. | ||
| * | ||
| * @param text Raw text to scan | ||
| * @param options Scanner configuration | ||
| */ | ||
| declare const scanInjectionPatterns: (text: string, options?: ScanOptions, context?: ScanContext) => ThreatReport[]; | ||
| declare const scanInjectionPatterns: (text: string, options?: ScanOptions) => ThreatReportWithoutLocation[]; | ||
| /** | ||
| * Invisible-character detector. | ||
| * Invisible character detector. | ||
| * | ||
| * Emits one primary span-level rule using precedence: | ||
| * This detector emits **span-level threats** with the following precedence: | ||
| * | ||
@@ -317,5 +421,13 @@ * PSU004 → Unicode tag payload | ||
| * | ||
| * PSU002 is emitted independently for boundary manipulation. | ||
| * Additionally: | ||
| * | ||
| * PSU002 is emitted independently for **token boundary manipulation** | ||
| * where an invisible character appears inside a visible token. | ||
| * | ||
| * Span semantics: | ||
| * | ||
| * • offendingText represents the **entire invisible sequence** | ||
| * • spans are **not merged across newline boundaries** | ||
| */ | ||
| declare const scanInvisibleChars: (text: string, options?: ScanOptions, context?: ScanContext) => ThreatReport[]; | ||
| declare const scanInvisibleChars: (text: string, options?: ScanOptions) => ThreatReportWithoutLocation[]; | ||
| /** | ||
@@ -330,6 +442,6 @@ * Attempts to decode Unicode tag characters into ASCII text. | ||
| * | ||
| * ASCII = codePoint - 0xE0000 | ||
| * ASCII = codePoint − 0xE0000 | ||
| * | ||
| * Attackers can use this mechanism to embed hidden instructions | ||
| * or metadata inside otherwise invisible text streams. | ||
| * This mechanism has been abused in multiple security reports to embed | ||
| * hidden instructions inside invisible character streams. | ||
| * | ||
@@ -343,31 +455,42 @@ * This decoder performs a best-effort extraction. | ||
| * | ||
| * Detects characters that change under NFKC normalization. | ||
| * Detects characters whose representation changes under **NFKC normalization**. | ||
| * | ||
| * Unicode normalization can transform visually similar characters | ||
| * into canonical equivalents. When user-visible text differs from | ||
| * its normalized form, this may indicate: | ||
| * Unicode normalization may transform visually similar or compatibility | ||
| * characters into canonical equivalents. When displayed text differs from | ||
| * its normalized form, this can introduce ambiguity between what users see | ||
| * and what downstream systems interpret. | ||
| * | ||
| * Such situations may indicate: | ||
| * | ||
| * - compatibility glyph usage | ||
| * - spoofing attempts | ||
| * - homoglyph confusion | ||
| * - prompt-smuggling techniques | ||
| * - content-validation bypass | ||
| * - prompt smuggling techniques | ||
| * - validation bypass in downstream processing pipelines | ||
| * | ||
| * Detection model: | ||
| * - Compare each character against its NFKC-normalized form | ||
| * - Group adjacent normalization-sensitive characters into a span | ||
| * - Emit one threat per span | ||
| * | ||
| * NOTE: | ||
| * This is intentionally heuristic. Many normalization changes are | ||
| * legitimate in multilingual text, but normalization-sensitive spans | ||
| * in prompts or code should be surfaced for inspection. | ||
| * 1. Normalize the text using **NFKC** | ||
| * 2. Iterate over characters in the original text | ||
| * 3. Identify characters whose normalized form differs | ||
| * 4. Group adjacent normalization-sensitive characters into spans | ||
| * 5. Emit one threat per span | ||
| * | ||
| * Severity heuristic: | ||
| * | ||
| * - **PSN001 (LOW)** | ||
| * Compatibility normalization producing simple ASCII text. | ||
| * | ||
| * - **PSN002 (MEDIUM)** | ||
| * More complex normalization transformations. | ||
| * | ||
| * Span semantics: | ||
| * offendingText = original span | ||
| * decodedPayload = normalized span | ||
| * | ||
| * Rule: | ||
| * PSN001 — Normalization-sensitive text detected | ||
| * offendingText = original span | ||
| * decodedPayload = normalized span | ||
| * | ||
| * Normalization can expand characters (example: `ff → ff`), therefore | ||
| * the normalized payload is computed from the entire span. | ||
| */ | ||
| declare const scanNormalization: (text: string, options?: ScanOptions, context?: ScanContext) => ThreatReport[]; | ||
| declare const scanNormalization: (text: string, options?: ScanOptions) => ThreatReportWithoutLocation[]; | ||
@@ -379,3 +502,4 @@ /** biome-ignore-all lint/suspicious/noAssignInExpressions: iterating over regex matches */ | ||
| * | ||
| * Detects techniques used to conceal instructions or data inside text. | ||
| * Detects techniques used to conceal instructions or payloads | ||
| * inside otherwise harmless-looking text. | ||
| * | ||
@@ -386,12 +510,13 @@ * Rules emitted: | ||
| * PSS002 — Base64 payload with readable content (MEDIUM) | ||
| * PSS006 — Hex-encoded payload with readable content (MEDIUM) | ||
| * PSS003 — Hidden Markdown comment (LOW) | ||
| * PSS004 — Invisible Markdown link (LOW) | ||
| * PSS005 — Hidden HTML container (LOW) | ||
| * | ||
| * Span semantics: | ||
| * offendingText = entire suspicious region | ||
| * decodedPayload = recovered payload when available | ||
| * | ||
| * Context is intentionally mutable so detectors can share `lineOffsets`. | ||
| * offendingText = suspicious region | ||
| * decodedPayload = recovered payload when available | ||
| */ | ||
| declare const scanSmuggling: (text: string, options?: ScanOptions, context?: ScanContext) => ThreatReport[]; | ||
| declare const scanSmuggling: (text: string, options?: ScanOptions) => ThreatReportWithoutLocation[]; | ||
@@ -402,32 +527,104 @@ /** | ||
| * Detects unsafe usage of Unicode Bidirectional (BIDI) control characters | ||
| * that may cause visual ordering to differ from logical ordering. | ||
| * that can cause the *visual order* of text to differ from its *logical order*. | ||
| * | ||
| * These attacks allow malicious code or instructions to appear benign to | ||
| * reviewers while executing differently when interpreted by compilers, | ||
| * interpreters, or LLMs. | ||
| * | ||
| * Detection rules: | ||
| * | ||
| * PST001 — Matched BIDI override sequence | ||
| * PST002 — Unterminated BIDI override sequence | ||
| */ | ||
| declare const scanTrojanSource: (text: string, options?: ScanOptions, context?: ScanContext) => ThreatReport[]; | ||
| declare const scanTrojanSource: (text: string, options?: ScanOptions) => ThreatReportWithoutLocation[]; | ||
| /** | ||
| * Core scanning entry point. | ||
| * Computes line start offsets for a string. | ||
| * | ||
| * Executes all enabled detectors in priority order: | ||
| * Each entry represents the character index where a new line begins. | ||
| * The first entry is always `0`. | ||
| * | ||
| * 1. Trojan Source (BIDI logic manipulation) | ||
| * 2. Invisible characters | ||
| * 3. Homoglyph spoofing | ||
| * 4. Unicode normalization anomalies | ||
| * 5. Smuggling techniques | ||
| * Example: | ||
| * "a\nb\nc" → [0, 2, 4] | ||
| * | ||
| * The provided `context` object is shared across detectors and may be | ||
| * mutated for performance optimizations (e.g., caching line offsets). | ||
| * This enables fast index → (line, column) mapping without repeatedly | ||
| * scanning the entire string. | ||
| */ | ||
| declare const getLineOffsets: (text: string) => number[]; | ||
| /** | ||
| * Resolves a character index into a line/column location. | ||
| * | ||
| * Uses binary search over precomputed line offsets for O(log n) lookup. | ||
| * | ||
| * Context provides: | ||
| * - baseLine | ||
| * - baseCol | ||
| * - lineOffsets | ||
| * | ||
| * `baseLine` and `baseCol` allow this function to operate correctly when | ||
| * scanning substrings that originate from a larger document. | ||
| */ | ||
| declare const getLocForIndex: (index: number, context: Required<ScanContext>) => { | ||
| line: number; | ||
| column: number; | ||
| index: number; | ||
| }; | ||
| /** | ||
| * Enriches ThreatReports with human-readable line/column locations. | ||
| * | ||
| * PromptShield detectors operate on absolute character offsets for | ||
| * performance and editor compatibility (e.g., Tiptap, LSP, AST tools). | ||
| * | ||
| * However, human-facing environments such as: | ||
| * | ||
| * - CLI output | ||
| * - CI diagnostics | ||
| * - logs | ||
| * - static analysis reports | ||
| * | ||
| * require line and column information. | ||
| * | ||
| * This helper converts offset-based threat ranges into location-aware | ||
| * structures in a single pass. | ||
| * | ||
| * The function computes line offsets once and resolves both start and | ||
| * end positions using binary search (`getLocForIndex`). | ||
| * | ||
| * This approach is significantly more efficient than resolving locations | ||
| * during detection or performing repeated scans of the input text. | ||
| * | ||
| * @param threats | ||
| * List of ThreatReports produced by `scan()`. These must contain | ||
| * offset-based ranges (`range.start`, `range.end`). | ||
| * | ||
| * @param text | ||
| * The original scanned text used to generate the threats. | ||
| * | ||
| * @param context | ||
| * Optional scan context for offset translation. Supports: | ||
| * - `baseLine` | ||
| * - `baseCol` | ||
| * | ||
| * This is useful when scanning substrings embedded inside a larger | ||
| * document (e.g., editor buffers, LSP fragments). | ||
| * | ||
| * @returns | ||
| * A new array of ThreatReports where each threat includes | ||
| * `loc.start` and `loc.end` describing the resolved line/column | ||
| * positions. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { scan } from '@promptshield/core'; | ||
| * const result = scan(text); | ||
| * const threats = enrichWithLoc(result.threats, text); | ||
| * | ||
| * const result = scan("Hello\u200BWorld"); | ||
| * if (!result.isClean) { | ||
| * console.log(result.threats); | ||
| * } | ||
| * console.log(threats[0].loc); | ||
| * // { | ||
| * // start: { line: 2, column: 5, index: 17 }, | ||
| * // end: { line: 2, column: 8, index: 20 } | ||
| * // } | ||
| * ``` | ||
| */ | ||
| declare const scan: (text: string, options?: ScanOptions, context?: ScanContext) => ScanResult; | ||
| declare const enrichWithLocation: (threats: ThreatReportWithoutLocation[], text: string, context?: Omit<ScanContext, "lineOffsets">) => ThreatReport[]; | ||
| export { type Detector, SEVERITY_MAP, type ScanContext, type ScanOptions, type ScanResult, type ScanStats, type Severity, ThreatCategory, type ThreatLoc, type ThreatReport, decodeUnicodeTags, scan, scanHomoglyphs, scanInjectionPatterns, scanInvisibleChars, scanNormalization, scanSmuggling, scanTrojanSource }; | ||
| export { type Detector, type IgnoreChecker, type Location, SEVERITY_MAP, type ScanContext, type ScanOptions, type ScanResult, type Severity, ThreatCategory, type ThreatReport, type ThreatReportWithoutLocation, decodeUnicodeTags, enrichWithLocation, getLineOffsets, getLocForIndex, scan, scanHomoglyphs, scanInjectionPatterns, scanInvisibleChars, scanNormalization, scanSmuggling, scanTrojanSource }; |
+3
-4
@@ -1,4 +0,3 @@ | ||
| "use strict";var x=Object.defineProperty;var j=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var M=Object.prototype.hasOwnProperty;var N=(e,t)=>{for(var r in t)x(e,r,{get:t[r],enumerable:!0})},w=(e,t,r,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of D(t))!M.call(e,n)&&n!==r&&x(e,n,{get:()=>t[n],enumerable:!(i=j(t,n))||i.enumerable});return e};var z=e=>w(x({},"__esModule",{value:!0}),e);var re={};N(re,{SEVERITY_MAP:()=>G,ThreatCategory:()=>U,decodeUnicodeTags:()=>A,scan:()=>te,scanHomoglyphs:()=>v,scanInjectionPatterns:()=>R,scanInvisibleChars:()=>C,scanNormalization:()=>O,scanSmuggling:()=>L,scanTrojanSource:()=>P});module.exports=z(re);var E=require("perf_hooks");var G={CRITICAL:1,HIGH:2,MEDIUM:3,LOW:4},U=(o=>(o.Invisible="INVISIBLE_CHAR",o.Homoglyph="HOMOGLYPH",o.Smuggling="SMUGGLING",o.Injection="PROMPT_INJECTION",o.Trojan="TROJAN_SOURCE",o.Normalization="NORMALIZATION",o))(U||{});var u=e=>{let t=[0];for(let r=0;r<e.length;r++)e[r]===` | ||
| `&&t.push(r+1);return t},p=(e,t)=>{let{lineOffsets:r=[0],baseLine:i=1,baseCol:n=1}=t,s=0,o=r.length-1;for(;s<=o;){let a=s+o>>1;r[a]<=e?s=a+1:o=a-1}let d=Math.max(o,0);return{line:i+d,column:e-r[d]+(d===0?n:1),index:e}};var B=/\p{Script=Latin}/u,_=/\p{Script=Cyrillic}/u,k=/\p{Script=Greek}/u,$=/[\p{L}\p{N}_]+/gu,v=(e,t={},r={})=>{let i=new RegExp($),n=i.exec(e);if(!n)return[];let s=[];for(r.lineOffsets=r.lineOffsets??u(e);n!==null;){let o=n[0],d=n.index,a=B.test(o),l=_.test(o),c=k.test(o);if(a&&(l||c)){let g=[];if(a&&g.push("Latin"),l&&g.push("Cyrillic"),c&&g.push("Greek"),s.push({ruleId:"PSH001",category:"HOMOGLYPH",severity:"CRITICAL",message:`Mixed-script homoglyph detected: "${o}" (${g.join(" + ")})`,referenceUrl:"https://promptshield.js.org/docs/detectors/homoglyph#PSH001",loc:p(d,r),offendingText:o,readableLabel:`[Mixed-Script] ${o}`,suggestion:"Replace visually similar characters with characters from a single script."}),t?.stopOnFirstThreat)return s}n=i.exec(e)}return s};var W=[{id:"PSI001",severity:"CRITICAL",message:"Prompt injection attempt: ignore previous instructions",regex:/ignore\s+previous\s+instructions/i,normalizedPattern:"ignorepreviousinstructions"},{id:"PSI002",severity:"CRITICAL",message:"Attempt to reveal system prompt",regex:/reveal\s+(system|hidden)\s+prompt/i,normalizedPattern:"revealsystemprompt"},{id:"PSI003",severity:"HIGH",message:"Attempt to disable guardrails",regex:/disable\s+(guardrails|safety)/i,normalizedPattern:"disableguardrails"},{id:"PSI004",severity:"HIGH",message:"System override instruction detected",regex:/override\s+(system|instructions)/i,normalizedPattern:"overridesysteminstructions"}],K=e=>e.toLowerCase().replace(/[^a-z\s]/g,"").replace(/\s+/g,""),R=(e,t={},r={})=>{let i=[];r.lineOffsets=r.lineOffsets??u(e);let n=e.split(` | ||
| `),s=0;for(let o of n){let d=K(o);for(let a of W){let l=a.regex.exec(o);if(l){if(i.push({ruleId:a.id,category:"PROMPT_INJECTION",severity:a.severity,message:a.message,offendingText:l[0],loc:p(s+l.index,r),readableLabel:"[Injection]",suggestion:"Remove instruction-override language from prompts or user content.",referenceUrl:`https://promptshield.js.org/docs/detectors/injection-patterns#${a.id}`}),t.stopOnFirstThreat)return i;continue}if(d.includes(a.normalizedPattern)&&(i.push({ruleId:a.id,category:"PROMPT_INJECTION",severity:a.severity,message:`${a.message} (obfuscated spacing detected)`,offendingText:o.trim(),loc:p(s,r),readableLabel:"[Injection]",suggestion:"Obfuscated instruction detected. Inspect and remove malicious content.",referenceUrl:`https://promptshield.js.org/docs/detectors/injection-patterns#${a.id}`}),t.stopOnFirstThreat))return i}s+=o.length+1}return i};var F={"\u200B":"ZWSP","\u200C":"ZWNJ","\u200D":"ZWJ","\uFEFF":"BOM",\u3164:"HF",\uFFA0:"HHF"},X=/([\u200B-\u200D\uFEFF\u3164\uFFA0]|\uDB40[\uDC00-\uDC7F])/gu,Z=16,C=(e,t={},r={})=>{let i=new RegExp(X),n=i.exec(e);if(!n||t?.minSeverity==="CRITICAL")return[];let s=[];r.lineOffsets=r.lineOffsets??u(e);let o=-1,d=-1,a=()=>{o=-1,d=-1},l=()=>{if(o===-1)return;let c=e.slice(o,d),g=A(c),f=[...c].map(S=>{let b=S.codePointAt(0);return F[S]||`U+${b?.toString(16).toUpperCase()}`}),m=p(o,r);if(g){s.push({ruleId:"PSU004",category:"INVISIBLE_CHAR",severity:"HIGH",message:"Unicode tag characters encode hidden ASCII content inside invisible text.",referenceUrl:"https://promptshield.js.org/docs/detectors/invisible-chars#PSU004",loc:m,offendingText:c,decodedPayload:g,readableLabel:"[TAG_PAYLOAD]",suggestion:"Remove Unicode tag characters containing hidden text."}),a();return}if(t.minSeverity!=="HIGH"){if(c.length>=Z){s.push({ruleId:"PSU005",category:"INVISIBLE_CHAR",severity:"MEDIUM",message:"Excessive invisible characters detected. Large invisible sequences are commonly used for padding or obfuscation.",referenceUrl:"https://promptshield.js.org/docs/detectors/invisible-chars#PSU005",loc:m,offendingText:c,readableLabel:`[${f.join(" ")}]`,suggestion:"Remove unnecessary invisible characters."}),a();return}t.minSeverity!=="MEDIUM"&&(s.push({ruleId:"PSU001",category:"INVISIBLE_CHAR",severity:"LOW",message:"Invisible Unicode characters detected. These characters can alter tokenization and prompt interpretation without being visible.",referenceUrl:"https://promptshield.js.org/docs/detectors/invisible-chars#PSU001",loc:m,offendingText:c,readableLabel:`[${f.join(" ")}]`,suggestion:"Remove invisible characters to ensure the prompt text is interpreted exactly as written."}),a())}};for(;n!==null;){let c=n.index,g=n[0];if(c>0&&c<e.length-1){let f=e[c-1],m=e[c+g.length];if(f?.trim()&&m?.trim()&&(s.push({ruleId:"PSU002",category:"INVISIBLE_CHAR",severity:"HIGH",message:"Invisible character detected inside a visible token. This can manipulate token boundaries or bypass validation.",referenceUrl:"https://promptshield.js.org/docs/detectors/invisible-chars#PSU002",loc:p(c,r),offendingText:g,readableLabel:`[${F[g]}]`||"[INVISIBLE]",suggestion:"Remove invisible characters embedded within words."}),t.stopOnFirstThreat))return s}if(o===-1)o=c,d=c+g.length;else if(c===d)d+=g.length;else{if(l(),t.stopOnFirstThreat&&s.length)return s;o=c,d=c+g.length}n=i.exec(e)}return l(),s},A=e=>{let t="",r=!1;for(let i of e){let n=i.codePointAt(0);if(n>=917504&&n<=917631){let s=n-917504;s>=32&&s<=126&&(t+=String.fromCharCode(s),r=!0)}}return r?t:void 0};var O=(e,t={},r={})=>{if(t.minSeverity==="CRITICAL")return[];let i=[],n=e.normalize("NFKC");if(e===n)return[];r.lineOffsets=r.lineOffsets??u(e);let s=0,o=-1,d=-1,a="",l=()=>{if(o===-1)return;let c=e.slice(o,d);i.push({ruleId:"PSN001",category:"NORMALIZATION",severity:"HIGH",message:"Text changes under Unicode NFKC normalization. This may cause ambiguity between displayed and interpreted content.",referenceUrl:"https://promptshield.js.org/docs/detectors/normalization#PSN001",loc:p(o,r),offendingText:c,decodedPayload:a,readableLabel:"[NFKC_DIFF]",suggestion:"Replace with normalized text to avoid ambiguity."}),o=-1,d=-1,a=""};for(let c of e){let g=c.normalize("NFKC");if(c!==g){if(o===-1?(o=s,d=s+c.length):d+=c.length,a+=g,t.stopOnFirstThreat)return l(),i}else l();s+=c.length}return l(),i};var V=/(?:[A-Za-z0-9+/]{4}){8,}(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?/g,J=/<!--[\s\S]*?-->/g,Y=/\[\s*\]\([^)]+\)/g,q=/([\u200B-\u200D\u2060\uFEFF\u3164\uFFA0]+)/g,Q=e=>{try{let t=Buffer.from(e,"base64").toString("utf8");if(!t)return null;let r=0;for(let n of t){let s=n.charCodeAt(0);s>=32&&s<=126&&r++}return r/t.length>=.7?t:null}catch{return null}},L=(e,t={},r={})=>{if(t.minSeverity==="CRITICAL")return[];let i=[],n;r.lineOffsets=r.lineOffsets??u(e);let s=new RegExp(q);for(;(n=s.exec(e))!==null;){let l=n[0];if(l.length<8||l.length>4096)continue;let c=Array.from(new Set(l.split("")));if(c.length<2||c.length>3)continue;let[g,f]=c,m=[{zero:g,one:f},{zero:f,one:g}];for(let{zero:S,one:b}of m){let I="";for(let h of l)h===S?I+="0":h===b&&(I+="1");let y="";for(let h=0;h<I.length;h+=8){let H=I.slice(h,h+8);if(H.length!==8)continue;let T=parseInt(H,2);T>=32&&T<=126&&(y+=String.fromCharCode(T))}if(y.length>=3){if(i.push({ruleId:"PSS001",category:"SMUGGLING",severity:"HIGH",message:"Detected hidden steganography message encoded in invisible characters.",loc:p(n.index,r),offendingText:l,decodedPayload:y,readableLabel:`[Hidden]: ${y.slice(0,50)}...`,suggestion:"Invisible-character encoding detected. Inspect hidden content.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS001"}),t.stopOnFirstThreat)return i;break}}}if(t.minSeverity==="HIGH")return i;let o=new RegExp(V);for(;(n=o.exec(e))!==null;){let l=n[0];if(l.length<24)continue;let c=Q(l);if(c&&(i.push({ruleId:"PSS002",category:"SMUGGLING",severity:"MEDIUM",message:"Detected Base64 payload containing readable content.",loc:p(n.index,r),offendingText:l,decodedPayload:c,readableLabel:`[Base64]: ${c.slice(0,50)}...`,suggestion:"Decoded Base64 contains readable text. Inspect payload.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS002"}),t.stopOnFirstThreat))return i}if(t.minSeverity==="MEDIUM")return i;let d=new RegExp(J);for(;(n=d.exec(e))!==null;)if(i.push({ruleId:"PSS003",category:"SMUGGLING",severity:"LOW",message:"Detected hidden Markdown comment.",loc:p(n.index,r),offendingText:n[0],readableLabel:"[Hidden Comment]",suggestion:"Comments are not visible in rendered Markdown but can carry instructions.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS003"}),t.stopOnFirstThreat)return i;let a=new RegExp(Y);for(;(n=a.exec(e))!==null;)if(i.push({ruleId:"PSS004",category:"SMUGGLING",severity:"LOW",message:"Detected empty Markdown link (invisible in rendered output).",loc:p(n.index,r),offendingText:n[0],readableLabel:"[Empty Link]",suggestion:"Empty links can be used to hide URLs or data.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS004"}),t.stopOnFirstThreat)return i;return i};var ee={"\u202A":"PUSH","\u202B":"PUSH","\u202D":"PUSH","\u202E":"PUSH","\u2066":"PUSH","\u2067":"PUSH","\u2068":"PUSH","\u202C":"POP","\u2069":"POP"},P=(e,t={},r={})=>{let i=[];r.lineOffsets=r.lineOffsets??u(e);let n=e.split(` | ||
| `),s=0;for(let o=0;o<n.length;o++){let d=n[o],a=null;for(let l=0;l<d.length;l++){let c=d[l],g=ee[c];if(g==="PUSH"&&a===null)a=s;else if(g==="POP"&&a!==null){let f=s+1,m=e.slice(a,f),S=e.slice(a+1,s);if(i.push({ruleId:"PST001",category:"TROJAN_SOURCE",severity:"CRITICAL",message:"Bidirectional override characters detected (Trojan Source). These characters can visually reorder text and mislead readers.",referenceUrl:"https://promptshield.js.org/docs/detectors/trojan-source#PST001",loc:p(a,r),offendingText:m,decodedPayload:S,readableLabel:"[BIDI_OVERRIDE]",suggestion:"Remove bidirectional control characters from the source."}),a=null,t.stopOnFirstThreat)return i}s++}if(a!==null){let l=s,c=e.slice(a,l),g=e.slice(a+1,l);if(i.push({ruleId:"PST002",category:"TROJAN_SOURCE",severity:"CRITICAL",message:"Unterminated bidirectional override sequence detected (Trojan Source). This may cause visual and logical text order to differ.",referenceUrl:"https://promptshield.js.org/docs/detectors/trojan-source#PST002",loc:p(a,r),offendingText:c,decodedPayload:g,readableLabel:"[BIDI_UNTERMINATED]",suggestion:"Remove BIDI control characters or ensure they are properly terminated within the same line."}),t.stopOnFirstThreat)return i}s++}return i};var te=(e,t={},r={})=>{let i=E.performance.now(),n=[],s=[];t.disableTrojan||s.push(P),t.disableInvisible||s.push(C),t.disableHomoglyphs||s.push(v),t.disableNormalization||s.push(O),t.disableSmuggling||s.push(L),t.disableInjectionPatterns||s.push(R);for(let d of s){let a=d(e,t,r);if(n.push(...a),t.stopOnFirstThreat&&a.length>0)break}let o=E.performance.now();return{threats:n,stats:{durationMs:o-i,totalChars:e.length},isClean:n.length===0}};0&&(module.exports={SEVERITY_MAP,ThreatCategory,decodeUnicodeTags,scan,scanHomoglyphs,scanInjectionPatterns,scanInvisibleChars,scanNormalization,scanSmuggling,scanTrojanSource}); | ||
| "use strict";var T=Object.defineProperty;var M=Object.getOwnPropertyDescriptor;var W=Object.getOwnPropertyNames;var D=Object.prototype.hasOwnProperty;var N=(o,e)=>{for(var r in e)T(o,r,{get:e[r],enumerable:!0})},k=(o,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of W(e))!D.call(o,a)&&a!==r&&T(o,a,{get:()=>e[a],enumerable:!(n=M(e,a))||n.enumerable});return o};var z=o=>k(T({},"__esModule",{value:!0}),o);var ce={};N(ce,{SEVERITY_MAP:()=>b,ThreatCategory:()=>x,decodeUnicodeTags:()=>U,enrichWithLocation:()=>A,getLineOffsets:()=>F,getLocForIndex:()=>O,scan:()=>ae,scanHomoglyphs:()=>R,scanInjectionPatterns:()=>v,scanInvisibleChars:()=>L,scanNormalization:()=>C,scanSmuggling:()=>P,scanTrojanSource:()=>E});module.exports=z(ce);var b={CRITICAL:1,HIGH:2,MEDIUM:3,LOW:4},x=(i=>(i.Invisible="INVISIBLE_CHAR",i.Homoglyph="HOMOGLYPH",i.Smuggling="SMUGGLING",i.Injection="PROMPT_INJECTION",i.Trojan="TROJAN_SOURCE",i.Normalization="NORMALIZATION",i))(x||{});var _=/\p{Script=Latin}/u,G=/\p{Script=Cyrillic}/u,B=/\p{Script=Greek}/u,$=/[\p{L}\p{N}_]+/gu,R=(o,e={})=>{let r=new RegExp($),n=r.exec(o);if(!n)return[];let a=[];for(;n!==null;){let t=n[0],i=n.index,u=_.test(t),g=G.test(t),c=B.test(t);if(u&&(g||c)){let s=[];u&&s.push("Latin"),g&&s.push("Cyrillic"),c&&s.push("Greek");let l=i,d=l+t.length;if(a.push({ruleId:"PSH001",category:"HOMOGLYPH",severity:"CRITICAL",message:`Mixed-script homoglyph detected: "${t}" (${s.join(" + ")})`,referenceUrl:"https://promptshield.js.org/docs/detectors/homoglyph#PSH001",range:{start:l,end:d},offendingText:t,readableLabel:`[Mixed-Script] ${t}`,suggestion:"Replace visually similar characters with characters from a single script."}),e?.stopOnFirstThreat&&!e.ignoreChecker?.(i,d))return a}n=r.exec(o)}return a};var K=[{id:"PSI001",type:"override",severity:"CRITICAL",message:"Prompt injection attempt: ignore previous instructions",regex:/ignore\s+(all\s+)?previous\s+instructions/gi,normalizedPattern:"ignorepreviousinstructions"},{id:"PSI002",type:"exfiltration",severity:"CRITICAL",message:"Attempt to reveal system prompt",regex:/(reveal|show|display|print)\s+(the\s+)?(system|hidden)\s+prompt/gi,normalizedPattern:"revealsystemprompt"},{id:"PSI003",type:"guardrail-bypass",severity:"HIGH",message:"Attempt to disable guardrails or safety protections",regex:/disable\s+(the\s+)?(guardrails|safety|safeguards)/gi,normalizedPattern:"disableguardrails"},{id:"PSI004",type:"override",severity:"HIGH",message:"System instruction override detected",regex:/override\s+(the\s+)?(system\s+)?(instructions|rules)/gi,normalizedPattern:"overridesysteminstructions"},{id:"PSI005",type:"override",severity:"CRITICAL",message:"Prompt injection attempt: ignore system prompt",regex:/ignore\s+(the\s+)?system\s+prompt/gi,normalizedPattern:"ignoresystemprompt"},{id:"PSI006",type:"override",severity:"CRITICAL",message:"Instruction override: follow attacker-provided instructions",regex:/follow\s+(my|these)\s+instructions/gi,normalizedPattern:"followmyinstructions"},{id:"PSI007",type:"role-override",severity:"HIGH",message:"Role override instruction detected",regex:/(you\s+are\s+now|act\s+as)\s+/gi,normalizedPattern:"youarenow"},{id:"PSI008",type:"exfiltration",severity:"CRITICAL",message:"Attempt to reveal hidden instructions",regex:/(reveal|show|display)\s+(hidden|internal)\s+instructions/gi,normalizedPattern:"revealhiddeninstructions"}],X=o=>{let e=[],r=[];for(let n=0;n<o.length;n++){let a=o[n].normalize("NFKD").replace(/\p{M}+/gu,"").toLowerCase();for(let t of a)/[a-z]/.test(t)&&(e.push(t),r.push(n))}return{normalized:e.join(""),map:r}},v=(o,e={})=>{let r=[],{normalized:n,map:a}=X(o);for(let t of K){let i=[],u=new RegExp(t.regex),g;for(;(g=u.exec(o))!==null;){let d=g.index,h=d+g[0].length;if(i.push({ruleId:t.id,category:"PROMPT_INJECTION",severity:t.severity,message:`\`${t.type}\`: ${t.message}`,offendingText:g[0],range:{start:d,end:h},readableLabel:`[Injection (${t.type})]`,suggestion:"Remove instruction-override language from prompts or user content.",referenceUrl:`https://promptshield.js.org/docs/detectors/injection-patterns#${t.id}`}),e.stopOnFirstThreat&&b[t.severity]<=b[e.minSeverity??"LOW"]&&!e.ignoreChecker?.(d,h))return r.push(...i),r;g[0].length===0&&u.lastIndex++}let c=t.normalizedPattern.length,s=n.indexOf(t.normalizedPattern),l=0;for(;s!==-1;){let d=a[s],h=a[s+c-1]+1;for(;l<i.length&&i[l].range.end<d;)l++;let p=!1;for(let m=l;m<i.length;m++){let y=i[m];if(y.range.start>h)break;if(d===y.range.start&&h===y.range.end){p=!0;break}}if(!p){let m=o.slice(d,h);if(i.push({ruleId:t.id,category:"PROMPT_INJECTION",severity:t.severity,message:`\`${t.type}\`: ${t.message} (obfuscated form detected)`,offendingText:m,range:{start:d,end:h},readableLabel:`[Injection (${t.type})]`,suggestion:"Obfuscated instruction detected. Inspect and remove malicious content.",referenceUrl:`https://promptshield.js.org/docs/detectors/injection-patterns#${t.id}`}),e.stopOnFirstThreat&&b[t.severity]<=b[e.minSeverity??"LOW"]&&!e.ignoreChecker?.(d,h))return r.push(...i),r}s=n.indexOf(t.normalizedPattern,s+1)}r.push(...i)}return r};var H={"\u200B":"ZWSP","\u200C":"ZWNJ","\u200D":"ZWJ","\u2060":"WJ","\u180E":"MVS","\uFEFF":"BOM",\u3164:"HF",\uFFA0:"HHF"},V=/([\u200B-\u200D\u2060\u180E\uFEFF\u3164\uFFA0]|\uDB40[\uDC00-\uDC7F])/gu,Y=16,L=(o,e={})=>{let r=new RegExp(V),n=r.exec(o);if(!n||e?.minSeverity==="CRITICAL")return[];let a=[],t=-1,i=-1,u=()=>{t=-1,i=-1},g=()=>{if(t===-1)return;let c=o.slice(t,i),s=U(c),l=[...c].map(h=>{let p=h.codePointAt(0);return H[h]||`U+${p?.toString(16).toUpperCase()}`}),d;if(s?d={ruleId:"PSU004",category:"INVISIBLE_CHAR",severity:"HIGH",message:"Unicode tag characters encode hidden ASCII content inside invisible text.",referenceUrl:"https://promptshield.js.org/docs/detectors/invisible-chars#PSU004",range:{start:t,end:i},offendingText:c,decodedPayload:s,readableLabel:"[TAG_PAYLOAD]",suggestion:"Remove Unicode tag characters containing hidden text."}:e.minSeverity!=="HIGH"&&c.length>=Y?d={ruleId:"PSU005",category:"INVISIBLE_CHAR",severity:"MEDIUM",message:"Excessive invisible characters detected. Large invisible sequences are commonly used for padding or obfuscation.",referenceUrl:"https://promptshield.js.org/docs/detectors/invisible-chars#PSU005",range:{start:t,end:i},offendingText:c,readableLabel:`[${l.join(" ")}]`,suggestion:"Remove unnecessary invisible characters."}:e.minSeverity!=="MEDIUM"&&(d={ruleId:"PSU001",category:"INVISIBLE_CHAR",severity:"LOW",message:"Invisible Unicode characters detected. These characters can alter tokenization and prompt interpretation without being visible.",referenceUrl:"https://promptshield.js.org/docs/detectors/invisible-chars#PSU001",range:{start:t,end:i},offendingText:c,readableLabel:`[${l.join(" ")}]`,suggestion:"Remove invisible characters to ensure the prompt text is interpreted exactly as written."}),d&&(a.push(d),e.stopOnFirstThreat&&!e.ignoreChecker?.(d.range.start,d.range.end)))throw a;u()};try{for(;n!==null;){let c=n.index,s=n[0];if(c>0&&c<o.length-1){let l=o[c-1],d=o[c+s.length];if(l?.trim()&&d?.trim()){let h=c,p=c+s.length,m={ruleId:"PSU002",category:"INVISIBLE_CHAR",severity:"HIGH",message:"Invisible character detected inside a visible token. This can manipulate token boundaries or bypass validation.",referenceUrl:"https://promptshield.js.org/docs/detectors/invisible-chars#PSU002",range:{start:h,end:p},offendingText:s,readableLabel:`[${H[s]}]`||"[INVISIBLE]",suggestion:"Remove invisible characters embedded within words."};if(a.push(m),e.stopOnFirstThreat&&!e.ignoreChecker?.(h,p))return a}}t===-1?(t=c,i=c+s.length):c===i?i+=s.length:(g(),t=c,i=c+s.length),n=r.exec(o)}g()}catch(c){return c}return a},U=o=>{let e="",r=!1;for(let n of o){let a=n.codePointAt(0);if(a>=917504&&a<=917631){let t=a-917504;t>=32&&t<=126&&(e+=String.fromCharCode(t),r=!0)}}return r?e:void 0};var C=(o,e={})=>{if(e.minSeverity==="CRITICAL")return[];let r=[],n=o.normalize("NFKC");if(o===n)return[];let a=0,t=-1,i=-1,u=()=>{if(t===-1)return;let g=o.slice(t,i),c=g.normalize("NFKC"),s=/^[a-z0-9\s]+$/i.test(c),l=s?"PSN001":"PSN002",d=s?"LOW":"MEDIUM";r.push({ruleId:l,category:"NORMALIZATION",severity:d,message:"Text changes under Unicode NFKC normalization. This may cause ambiguity between displayed and interpreted content.",referenceUrl:`https://promptshield.js.org/docs/detectors/normalization#${l}`,range:{start:t,end:i},offendingText:g,decodedPayload:c,readableLabel:"[NFKC_DIFF]",suggestion:"Replace with normalized text to avoid ambiguity."}),t=-1,i=-1};for(let g of o){let c=g.normalize("NFKC");if(g!==c){if(t===-1?(t=a,i=a+g.length):i+=g.length,e.stopOnFirstThreat&&!e.ignoreChecker?.(a,a+g.length))return u(),r}else u();a+=g.length}return u(),r};var j=24,w=4096,Z=/(?:[A-Za-z0-9+/_-]{4}[\s\u200B-\u200D\u2060\uFEFF]*){8,}(?:[A-Za-z0-9+/_-]{2}==|[A-Za-z0-9+/_-]{3}=)?/g,q=/\b(?:[0-9a-fA-F]{2}){12,}\b/g,J=/<!--[\s\S]*?-->/g,Q=/\[\s*\]\([^)]+\)/g,ee=/<(details|template)\b[^><]{0,200}>[\s\S]{0,100000}?<\/\1>/gi,te=/<summary\b[^><]{0,200}>[\s\S]{0,20000}?<\/summary>/i,re=/([\u200B-\u200D\u2060\uFEFF\u3164\uFFA0]+)/g,ne=o=>{if(typeof Buffer<"u")return Buffer.from(o,"base64").toString("utf8");let e=atob(o),r=Uint8Array.from(e,n=>n.charCodeAt(0));return new TextDecoder().decode(r)},oe=o=>{try{let e=o.replace(/\s+/g,"");if(e.length<j||e.length>w)return null;let r=ne(e);if(!r)return null;let n=0;for(let t of r){let i=t.charCodeAt(0);i>=32&&i<=126&&n++}return n/r.length>=.7?r:null}catch{return null}},se=o=>{try{if(o.length<j)return null;let e=new Uint8Array(o.length/2);for(let t=0;t<e.length;t++)e[t]=parseInt(o.slice(t*2,t*2+2),16);let r=new TextDecoder().decode(e),n=0;for(let t of r){let i=t.charCodeAt(0);i>=32&&i<=126&&n++}return n/r.length>=.7?r:null}catch{return null}},P=(o,e={})=>{if(e.minSeverity==="CRITICAL")return[];let r=[],n,a=new RegExp(re);for(;(n=a.exec(o))!==null;){let s=n[0];if(s.length<8||s.length>w)continue;let l=Array.from(new Set([...s]));if(l.length<2||l.length>4)continue;let d=[];for(let h=0;h<l.length;h++)for(let p=0;p<l.length;p++)h!==p&&d.push({zero:l[h],one:l[p]});for(let{zero:h,one:p}of d){let m="";for(let f of s)f===h?m+="0":f===p&&(m+="1");let y="";for(let f=0;f<m.length;f+=8){let I=m.slice(f,f+8);if(I.length!==8)continue;let S=parseInt(I,2);S>=32&&S<=126&&(y+=String.fromCharCode(S))}if(y.length>=3){let f=n.index,I=f+s.length;if(r.push({ruleId:"PSS001",category:"SMUGGLING",severity:"HIGH",message:"Detected hidden steganography message encoded in invisible characters.",range:{start:f,end:I},offendingText:s,decodedPayload:y,readableLabel:`[Hidden]: ${y.slice(0,120)}...`,suggestion:"Invisible-character encoding detected. Inspect hidden content.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS001"}),e.stopOnFirstThreat&&!e.ignoreChecker?.(f,I))return r;break}}}if(e.minSeverity==="HIGH")return r;let t=new RegExp(Z);for(;(n=t.exec(o))!==null;){let s=n[0],l=oe(s);if(!l)continue;let d=n.index,h=d+s.length;if(r.push({ruleId:"PSS002",category:"SMUGGLING",severity:"MEDIUM",message:"Detected Base64 payload containing readable content.",range:{start:d,end:h},offendingText:s,decodedPayload:l,readableLabel:`[Base64]: ${l.slice(0,120)}...`,suggestion:"Decoded Base64 contains readable text. Inspect payload.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS002"}),e.stopOnFirstThreat&&!e.ignoreChecker?.(d,h))return r}let i=new RegExp(q);for(;(n=i.exec(o))!==null;){let s=n[0],l=se(s);if(!l)continue;let d=n.index,h=d+s.length;if(r.push({ruleId:"PSS006",category:"SMUGGLING",severity:"MEDIUM",message:"Detected hex-encoded payload containing readable content.",range:{start:d,end:h},offendingText:s,decodedPayload:l,readableLabel:`[HEX]: ${l.slice(0,120)}...`,suggestion:"Decoded hex contains readable text. Inspect payload.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS006"}),e.stopOnFirstThreat&&!e.ignoreChecker?.(d,h))return r}if(e.minSeverity==="MEDIUM")return r;let u=new RegExp(J);for(;(n=u.exec(o))!==null;){let s=n.index,l=s+n[0].length;if(r.push({ruleId:"PSS003",category:"SMUGGLING",severity:"LOW",message:"Detected hidden Markdown comment.",range:{start:s,end:l},offendingText:n[0],readableLabel:"[Hidden Comment]",suggestion:"Comments are not visible in rendered Markdown but can carry instructions.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS003"}),e.stopOnFirstThreat&&!e.ignoreChecker?.(s,l))return r}let g=new RegExp(Q);for(;(n=g.exec(o))!==null;){let s=n.index,l=s+n[0].length;if(r.push({ruleId:"PSS004",category:"SMUGGLING",severity:"LOW",message:"Detected empty Markdown link (invisible in rendered output).",range:{start:s,end:l},offendingText:n[0],readableLabel:"[Empty Link]",suggestion:"Empty links can be used to hide URLs or data.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS004"}),e.stopOnFirstThreat&&!e.ignoreChecker?.(s,l))return r}let c=new RegExp(ee);for(;(n=c.exec(o))!==null;){let s=n[0];if(/^<details/i.test(s)&&te.test(s))continue;let l=n.index,d=l+s.length;if(r.push({ruleId:"PSS005",category:"SMUGGLING",severity:"LOW",message:"Detected hidden HTML container potentially concealing content.",range:{start:l,end:d},offendingText:s,readableLabel:"[Hidden HTML]",suggestion:"Hidden containers may conceal instructions from rendered output.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS005"}),e.stopOnFirstThreat&&!e.ignoreChecker?.(l,d))return r}return r};var ie={"\u202A":"PUSH","\u202B":"PUSH","\u202D":"PUSH","\u202E":"PUSH","\u2066":"PUSH","\u2067":"PUSH","\u2068":"PUSH","\u202C":"POP","\u2069":"POP","\u200E":"MARK","\u200F":"MARK","\u061C":"MARK"},E=(o,e={})=>{let r=[],n=[],a=0;for(let t=0;t<o.length;){let i=o.codePointAt(t),u=String.fromCodePoint(i),g=ie[u];if(g==="PUSH")n.push(a);else if(g==="POP"&&n.length){let c=n.pop(),s=a+u.length,l=o.slice(c,s),d=o.slice(c+1,s-1);if(r.push({ruleId:"PST001",category:"TROJAN_SOURCE",severity:"CRITICAL",message:"Bidirectional override characters detected (Trojan Source). These characters can visually reorder text and mislead reviewers.",referenceUrl:"https://promptshield.js.org/docs/detectors/trojan-source#PST001",range:{start:c,end:s},offendingText:l,decodedPayload:d,readableLabel:"[BIDI_OVERRIDE]",suggestion:"Remove bidirectional control characters or replace them with visible equivalents."}),e.stopOnFirstThreat&&!e.ignoreChecker?.(c,s))return r}if((u===` | ||
| `||u==="\r")&&n.length){let c=n.pop(),s=a,l=o.slice(c,s),d=o.slice(c+1,s);if(r.push({ruleId:"PST002",category:"TROJAN_SOURCE",severity:"CRITICAL",message:"Unterminated bidirectional override sequence detected (Trojan Source). This may cause visual and logical text order to differ.",referenceUrl:"https://promptshield.js.org/docs/detectors/trojan-source#PST002",range:{start:c,end:s},offendingText:l,decodedPayload:d,readableLabel:"[BIDI_UNTERMINATED]",suggestion:"Ensure BIDI control characters are properly terminated within the same logical line or remove them entirely."}),e.stopOnFirstThreat&&!e.ignoreChecker?.(c,s))return r}a+=u.length,t+=u.length}for(;n.length;){let t=n.pop(),i=o.length,u=o.slice(t,i),g=o.slice(t+1);if(r.push({ruleId:"PST002",category:"TROJAN_SOURCE",severity:"CRITICAL",message:"Unterminated bidirectional override sequence detected (Trojan Source).",referenceUrl:"https://promptshield.js.org/docs/detectors/trojan-source#PST002",range:{start:t,end:i},offendingText:u,decodedPayload:g,readableLabel:"[BIDI_UNTERMINATED]",suggestion:"Remove or terminate bidirectional override characters before the end of the document."}),e.stopOnFirstThreat&&!e.ignoreChecker?.(t,i))return r}return r};var F=o=>{let e=[0];for(let r=0;r<o.length;r++)o[r]===` | ||
| `&&e.push(r+1);return e},O=(o,e)=>{let{lineOffsets:r,baseLine:n,baseCol:a}=e,t=0,i=r.length-1;for(;t<=i;){let g=t+i>>1;r[g]<=o?t=g+1:i=g-1}let u=Math.max(i,0);return{line:n+u,column:o-r[u]+(u===0?a:1),index:o}},A=(o,e,r={})=>{let n=F(e),{baseLine:a=1,baseCol:t=1}=r,i={lineOffsets:n,baseLine:a,baseCol:t};return o.map(u=>{let g=O(u.range.start,i),c=O(u.range.end,i);return{...u,range:{start:g,end:c}}})};var ae=(o,e={})=>{let r=[],n=[];e.disableTrojan||n.push(E),e.disableInvisible||n.push(L),e.disableHomoglyphs||n.push(R),e.disableNormalization||n.push(C),e.disableSmuggling||n.push(P),e.disableInjectionPatterns||n.push(v);for(let a of n){let t=a(o,e);if(r.push(...t),e.stopOnFirstThreat&&t.length>0)break}return{threats:A(r,o),isClean:r.length===0}};0&&(module.exports={SEVERITY_MAP,ThreatCategory,decodeUnicodeTags,enrichWithLocation,getLineOffsets,getLocForIndex,scan,scanHomoglyphs,scanInjectionPatterns,scanInvisibleChars,scanNormalization,scanSmuggling,scanTrojanSource}); |
+3
-4
@@ -1,4 +0,3 @@ | ||
| import{performance as H}from"perf_hooks";var X={CRITICAL:1,HIGH:2,MEDIUM:3,LOW:4},U=(s=>(s.Invisible="INVISIBLE_CHAR",s.Homoglyph="HOMOGLYPH",s.Smuggling="SMUGGLING",s.Injection="PROMPT_INJECTION",s.Trojan="TROJAN_SOURCE",s.Normalization="NORMALIZATION",s))(U||{});var u=e=>{let r=[0];for(let n=0;n<e.length;n++)e[n]===` | ||
| `&&r.push(n+1);return r},p=(e,r)=>{let{lineOffsets:n=[0],baseLine:a=1,baseCol:o=1}=r,t=0,s=n.length-1;for(;t<=s;){let i=t+s>>1;n[i]<=e?t=i+1:s=i-1}let d=Math.max(s,0);return{line:a+d,column:e-n[d]+(d===0?o:1),index:e}};var F=/\p{Script=Latin}/u,A=/\p{Script=Cyrillic}/u,j=/\p{Script=Greek}/u,D=/[\p{L}\p{N}_]+/gu,v=(e,r={},n={})=>{let a=new RegExp(D),o=a.exec(e);if(!o)return[];let t=[];for(n.lineOffsets=n.lineOffsets??u(e);o!==null;){let s=o[0],d=o.index,i=F.test(s),l=A.test(s),c=j.test(s);if(i&&(l||c)){let g=[];if(i&&g.push("Latin"),l&&g.push("Cyrillic"),c&&g.push("Greek"),t.push({ruleId:"PSH001",category:"HOMOGLYPH",severity:"CRITICAL",message:`Mixed-script homoglyph detected: "${s}" (${g.join(" + ")})`,referenceUrl:"https://promptshield.js.org/docs/detectors/homoglyph#PSH001",loc:p(d,n),offendingText:s,readableLabel:`[Mixed-Script] ${s}`,suggestion:"Replace visually similar characters with characters from a single script."}),r?.stopOnFirstThreat)return t}o=a.exec(e)}return t};var M=[{id:"PSI001",severity:"CRITICAL",message:"Prompt injection attempt: ignore previous instructions",regex:/ignore\s+previous\s+instructions/i,normalizedPattern:"ignorepreviousinstructions"},{id:"PSI002",severity:"CRITICAL",message:"Attempt to reveal system prompt",regex:/reveal\s+(system|hidden)\s+prompt/i,normalizedPattern:"revealsystemprompt"},{id:"PSI003",severity:"HIGH",message:"Attempt to disable guardrails",regex:/disable\s+(guardrails|safety)/i,normalizedPattern:"disableguardrails"},{id:"PSI004",severity:"HIGH",message:"System override instruction detected",regex:/override\s+(system|instructions)/i,normalizedPattern:"overridesysteminstructions"}],N=e=>e.toLowerCase().replace(/[^a-z\s]/g,"").replace(/\s+/g,""),R=(e,r={},n={})=>{let a=[];n.lineOffsets=n.lineOffsets??u(e);let o=e.split(` | ||
| `),t=0;for(let s of o){let d=N(s);for(let i of M){let l=i.regex.exec(s);if(l){if(a.push({ruleId:i.id,category:"PROMPT_INJECTION",severity:i.severity,message:i.message,offendingText:l[0],loc:p(t+l.index,n),readableLabel:"[Injection]",suggestion:"Remove instruction-override language from prompts or user content.",referenceUrl:`https://promptshield.js.org/docs/detectors/injection-patterns#${i.id}`}),r.stopOnFirstThreat)return a;continue}if(d.includes(i.normalizedPattern)&&(a.push({ruleId:i.id,category:"PROMPT_INJECTION",severity:i.severity,message:`${i.message} (obfuscated spacing detected)`,offendingText:s.trim(),loc:p(t,n),readableLabel:"[Injection]",suggestion:"Obfuscated instruction detected. Inspect and remove malicious content.",referenceUrl:`https://promptshield.js.org/docs/detectors/injection-patterns#${i.id}`}),r.stopOnFirstThreat))return a}t+=s.length+1}return a};var C={"\u200B":"ZWSP","\u200C":"ZWNJ","\u200D":"ZWJ","\uFEFF":"BOM",\u3164:"HF",\uFFA0:"HHF"},w=/([\u200B-\u200D\uFEFF\u3164\uFFA0]|\uDB40[\uDC00-\uDC7F])/gu,z=16,O=(e,r={},n={})=>{let a=new RegExp(w),o=a.exec(e);if(!o||r?.minSeverity==="CRITICAL")return[];let t=[];n.lineOffsets=n.lineOffsets??u(e);let s=-1,d=-1,i=()=>{s=-1,d=-1},l=()=>{if(s===-1)return;let c=e.slice(s,d),g=G(c),f=[...c].map(S=>{let b=S.codePointAt(0);return C[S]||`U+${b?.toString(16).toUpperCase()}`}),m=p(s,n);if(g){t.push({ruleId:"PSU004",category:"INVISIBLE_CHAR",severity:"HIGH",message:"Unicode tag characters encode hidden ASCII content inside invisible text.",referenceUrl:"https://promptshield.js.org/docs/detectors/invisible-chars#PSU004",loc:m,offendingText:c,decodedPayload:g,readableLabel:"[TAG_PAYLOAD]",suggestion:"Remove Unicode tag characters containing hidden text."}),i();return}if(r.minSeverity!=="HIGH"){if(c.length>=z){t.push({ruleId:"PSU005",category:"INVISIBLE_CHAR",severity:"MEDIUM",message:"Excessive invisible characters detected. Large invisible sequences are commonly used for padding or obfuscation.",referenceUrl:"https://promptshield.js.org/docs/detectors/invisible-chars#PSU005",loc:m,offendingText:c,readableLabel:`[${f.join(" ")}]`,suggestion:"Remove unnecessary invisible characters."}),i();return}r.minSeverity!=="MEDIUM"&&(t.push({ruleId:"PSU001",category:"INVISIBLE_CHAR",severity:"LOW",message:"Invisible Unicode characters detected. These characters can alter tokenization and prompt interpretation without being visible.",referenceUrl:"https://promptshield.js.org/docs/detectors/invisible-chars#PSU001",loc:m,offendingText:c,readableLabel:`[${f.join(" ")}]`,suggestion:"Remove invisible characters to ensure the prompt text is interpreted exactly as written."}),i())}};for(;o!==null;){let c=o.index,g=o[0];if(c>0&&c<e.length-1){let f=e[c-1],m=e[c+g.length];if(f?.trim()&&m?.trim()&&(t.push({ruleId:"PSU002",category:"INVISIBLE_CHAR",severity:"HIGH",message:"Invisible character detected inside a visible token. This can manipulate token boundaries or bypass validation.",referenceUrl:"https://promptshield.js.org/docs/detectors/invisible-chars#PSU002",loc:p(c,n),offendingText:g,readableLabel:`[${C[g]}]`||"[INVISIBLE]",suggestion:"Remove invisible characters embedded within words."}),r.stopOnFirstThreat))return t}if(s===-1)s=c,d=c+g.length;else if(c===d)d+=g.length;else{if(l(),r.stopOnFirstThreat&&t.length)return t;s=c,d=c+g.length}o=a.exec(e)}return l(),t},G=e=>{let r="",n=!1;for(let a of e){let o=a.codePointAt(0);if(o>=917504&&o<=917631){let t=o-917504;t>=32&&t<=126&&(r+=String.fromCharCode(t),n=!0)}}return n?r:void 0};var L=(e,r={},n={})=>{if(r.minSeverity==="CRITICAL")return[];let a=[],o=e.normalize("NFKC");if(e===o)return[];n.lineOffsets=n.lineOffsets??u(e);let t=0,s=-1,d=-1,i="",l=()=>{if(s===-1)return;let c=e.slice(s,d);a.push({ruleId:"PSN001",category:"NORMALIZATION",severity:"HIGH",message:"Text changes under Unicode NFKC normalization. This may cause ambiguity between displayed and interpreted content.",referenceUrl:"https://promptshield.js.org/docs/detectors/normalization#PSN001",loc:p(s,n),offendingText:c,decodedPayload:i,readableLabel:"[NFKC_DIFF]",suggestion:"Replace with normalized text to avoid ambiguity."}),s=-1,d=-1,i=""};for(let c of e){let g=c.normalize("NFKC");if(c!==g){if(s===-1?(s=t,d=t+c.length):d+=c.length,i+=g,r.stopOnFirstThreat)return l(),a}else l();t+=c.length}return l(),a};var B=/(?:[A-Za-z0-9+/]{4}){8,}(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?/g,_=/<!--[\s\S]*?-->/g,k=/\[\s*\]\([^)]+\)/g,$=/([\u200B-\u200D\u2060\uFEFF\u3164\uFFA0]+)/g,W=e=>{try{let r=Buffer.from(e,"base64").toString("utf8");if(!r)return null;let n=0;for(let o of r){let t=o.charCodeAt(0);t>=32&&t<=126&&n++}return n/r.length>=.7?r:null}catch{return null}},P=(e,r={},n={})=>{if(r.minSeverity==="CRITICAL")return[];let a=[],o;n.lineOffsets=n.lineOffsets??u(e);let t=new RegExp($);for(;(o=t.exec(e))!==null;){let l=o[0];if(l.length<8||l.length>4096)continue;let c=Array.from(new Set(l.split("")));if(c.length<2||c.length>3)continue;let[g,f]=c,m=[{zero:g,one:f},{zero:f,one:g}];for(let{zero:S,one:b}of m){let I="";for(let h of l)h===S?I+="0":h===b&&(I+="1");let y="";for(let h=0;h<I.length;h+=8){let x=I.slice(h,h+8);if(x.length!==8)continue;let T=parseInt(x,2);T>=32&&T<=126&&(y+=String.fromCharCode(T))}if(y.length>=3){if(a.push({ruleId:"PSS001",category:"SMUGGLING",severity:"HIGH",message:"Detected hidden steganography message encoded in invisible characters.",loc:p(o.index,n),offendingText:l,decodedPayload:y,readableLabel:`[Hidden]: ${y.slice(0,50)}...`,suggestion:"Invisible-character encoding detected. Inspect hidden content.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS001"}),r.stopOnFirstThreat)return a;break}}}if(r.minSeverity==="HIGH")return a;let s=new RegExp(B);for(;(o=s.exec(e))!==null;){let l=o[0];if(l.length<24)continue;let c=W(l);if(c&&(a.push({ruleId:"PSS002",category:"SMUGGLING",severity:"MEDIUM",message:"Detected Base64 payload containing readable content.",loc:p(o.index,n),offendingText:l,decodedPayload:c,readableLabel:`[Base64]: ${c.slice(0,50)}...`,suggestion:"Decoded Base64 contains readable text. Inspect payload.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS002"}),r.stopOnFirstThreat))return a}if(r.minSeverity==="MEDIUM")return a;let d=new RegExp(_);for(;(o=d.exec(e))!==null;)if(a.push({ruleId:"PSS003",category:"SMUGGLING",severity:"LOW",message:"Detected hidden Markdown comment.",loc:p(o.index,n),offendingText:o[0],readableLabel:"[Hidden Comment]",suggestion:"Comments are not visible in rendered Markdown but can carry instructions.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS003"}),r.stopOnFirstThreat)return a;let i=new RegExp(k);for(;(o=i.exec(e))!==null;)if(a.push({ruleId:"PSS004",category:"SMUGGLING",severity:"LOW",message:"Detected empty Markdown link (invisible in rendered output).",loc:p(o.index,n),offendingText:o[0],readableLabel:"[Empty Link]",suggestion:"Empty links can be used to hide URLs or data.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS004"}),r.stopOnFirstThreat)return a;return a};var K={"\u202A":"PUSH","\u202B":"PUSH","\u202D":"PUSH","\u202E":"PUSH","\u2066":"PUSH","\u2067":"PUSH","\u2068":"PUSH","\u202C":"POP","\u2069":"POP"},E=(e,r={},n={})=>{let a=[];n.lineOffsets=n.lineOffsets??u(e);let o=e.split(` | ||
| `),t=0;for(let s=0;s<o.length;s++){let d=o[s],i=null;for(let l=0;l<d.length;l++){let c=d[l],g=K[c];if(g==="PUSH"&&i===null)i=t;else if(g==="POP"&&i!==null){let f=t+1,m=e.slice(i,f),S=e.slice(i+1,t);if(a.push({ruleId:"PST001",category:"TROJAN_SOURCE",severity:"CRITICAL",message:"Bidirectional override characters detected (Trojan Source). These characters can visually reorder text and mislead readers.",referenceUrl:"https://promptshield.js.org/docs/detectors/trojan-source#PST001",loc:p(i,n),offendingText:m,decodedPayload:S,readableLabel:"[BIDI_OVERRIDE]",suggestion:"Remove bidirectional control characters from the source."}),i=null,r.stopOnFirstThreat)return a}t++}if(i!==null){let l=t,c=e.slice(i,l),g=e.slice(i+1,l);if(a.push({ruleId:"PST002",category:"TROJAN_SOURCE",severity:"CRITICAL",message:"Unterminated bidirectional override sequence detected (Trojan Source). This may cause visual and logical text order to differ.",referenceUrl:"https://promptshield.js.org/docs/detectors/trojan-source#PST002",loc:p(i,n),offendingText:c,decodedPayload:g,readableLabel:"[BIDI_UNTERMINATED]",suggestion:"Remove BIDI control characters or ensure they are properly terminated within the same line."}),r.stopOnFirstThreat)return a}t++}return a};var me=(e,r={},n={})=>{let a=H.now(),o=[],t=[];r.disableTrojan||t.push(E),r.disableInvisible||t.push(O),r.disableHomoglyphs||t.push(v),r.disableNormalization||t.push(L),r.disableSmuggling||t.push(P),r.disableInjectionPatterns||t.push(R);for(let d of t){let i=d(e,r,n);if(o.push(...i),r.stopOnFirstThreat&&i.length>0)break}let s=H.now();return{threats:o,stats:{durationMs:s-a,totalChars:e.length},isClean:o.length===0}};export{X as SEVERITY_MAP,U as ThreatCategory,G as decodeUnicodeTags,me as scan,v as scanHomoglyphs,R as scanInjectionPatterns,O as scanInvisibleChars,L as scanNormalization,P as scanSmuggling,E as scanTrojanSource}; | ||
| var I={CRITICAL:1,HIGH:2,MEDIUM:3,LOW:4},T=(i=>(i.Invisible="INVISIBLE_CHAR",i.Homoglyph="HOMOGLYPH",i.Smuggling="SMUGGLING",i.Injection="PROMPT_INJECTION",i.Trojan="TROJAN_SOURCE",i.Normalization="NORMALIZATION",i))(T||{});var j=/\p{Script=Latin}/u,w=/\p{Script=Cyrillic}/u,F=/\p{Script=Greek}/u,M=/[\p{L}\p{N}_]+/gu,x=(s,t={})=>{let r=new RegExp(M),n=r.exec(s);if(!n)return[];let d=[];for(;n!==null;){let e=n[0],i=n.index,u=j.test(e),g=w.test(e),a=F.test(e);if(u&&(g||a)){let o=[];u&&o.push("Latin"),g&&o.push("Cyrillic"),a&&o.push("Greek");let c=i,l=c+e.length;if(d.push({ruleId:"PSH001",category:"HOMOGLYPH",severity:"CRITICAL",message:`Mixed-script homoglyph detected: "${e}" (${o.join(" + ")})`,referenceUrl:"https://promptshield.js.org/docs/detectors/homoglyph#PSH001",range:{start:c,end:l},offendingText:e,readableLabel:`[Mixed-Script] ${e}`,suggestion:"Replace visually similar characters with characters from a single script."}),t?.stopOnFirstThreat&&!t.ignoreChecker?.(i,l))return d}n=r.exec(s)}return d};var W=[{id:"PSI001",type:"override",severity:"CRITICAL",message:"Prompt injection attempt: ignore previous instructions",regex:/ignore\s+(all\s+)?previous\s+instructions/gi,normalizedPattern:"ignorepreviousinstructions"},{id:"PSI002",type:"exfiltration",severity:"CRITICAL",message:"Attempt to reveal system prompt",regex:/(reveal|show|display|print)\s+(the\s+)?(system|hidden)\s+prompt/gi,normalizedPattern:"revealsystemprompt"},{id:"PSI003",type:"guardrail-bypass",severity:"HIGH",message:"Attempt to disable guardrails or safety protections",regex:/disable\s+(the\s+)?(guardrails|safety|safeguards)/gi,normalizedPattern:"disableguardrails"},{id:"PSI004",type:"override",severity:"HIGH",message:"System instruction override detected",regex:/override\s+(the\s+)?(system\s+)?(instructions|rules)/gi,normalizedPattern:"overridesysteminstructions"},{id:"PSI005",type:"override",severity:"CRITICAL",message:"Prompt injection attempt: ignore system prompt",regex:/ignore\s+(the\s+)?system\s+prompt/gi,normalizedPattern:"ignoresystemprompt"},{id:"PSI006",type:"override",severity:"CRITICAL",message:"Instruction override: follow attacker-provided instructions",regex:/follow\s+(my|these)\s+instructions/gi,normalizedPattern:"followmyinstructions"},{id:"PSI007",type:"role-override",severity:"HIGH",message:"Role override instruction detected",regex:/(you\s+are\s+now|act\s+as)\s+/gi,normalizedPattern:"youarenow"},{id:"PSI008",type:"exfiltration",severity:"CRITICAL",message:"Attempt to reveal hidden instructions",regex:/(reveal|show|display)\s+(hidden|internal)\s+instructions/gi,normalizedPattern:"revealhiddeninstructions"}],D=s=>{let t=[],r=[];for(let n=0;n<s.length;n++){let d=s[n].normalize("NFKD").replace(/\p{M}+/gu,"").toLowerCase();for(let e of d)/[a-z]/.test(e)&&(t.push(e),r.push(n))}return{normalized:t.join(""),map:r}},R=(s,t={})=>{let r=[],{normalized:n,map:d}=D(s);for(let e of W){let i=[],u=new RegExp(e.regex),g;for(;(g=u.exec(s))!==null;){let l=g.index,h=l+g[0].length;if(i.push({ruleId:e.id,category:"PROMPT_INJECTION",severity:e.severity,message:`\`${e.type}\`: ${e.message}`,offendingText:g[0],range:{start:l,end:h},readableLabel:`[Injection (${e.type})]`,suggestion:"Remove instruction-override language from prompts or user content.",referenceUrl:`https://promptshield.js.org/docs/detectors/injection-patterns#${e.id}`}),t.stopOnFirstThreat&&I[e.severity]<=I[t.minSeverity??"LOW"]&&!t.ignoreChecker?.(l,h))return r.push(...i),r;g[0].length===0&&u.lastIndex++}let a=e.normalizedPattern.length,o=n.indexOf(e.normalizedPattern),c=0;for(;o!==-1;){let l=d[o],h=d[o+a-1]+1;for(;c<i.length&&i[c].range.end<l;)c++;let p=!1;for(let m=c;m<i.length;m++){let y=i[m];if(y.range.start>h)break;if(l===y.range.start&&h===y.range.end){p=!0;break}}if(!p){let m=s.slice(l,h);if(i.push({ruleId:e.id,category:"PROMPT_INJECTION",severity:e.severity,message:`\`${e.type}\`: ${e.message} (obfuscated form detected)`,offendingText:m,range:{start:l,end:h},readableLabel:`[Injection (${e.type})]`,suggestion:"Obfuscated instruction detected. Inspect and remove malicious content.",referenceUrl:`https://promptshield.js.org/docs/detectors/injection-patterns#${e.id}`}),t.stopOnFirstThreat&&I[e.severity]<=I[t.minSeverity??"LOW"]&&!t.ignoreChecker?.(l,h))return r.push(...i),r}o=n.indexOf(e.normalizedPattern,o+1)}r.push(...i)}return r};var v={"\u200B":"ZWSP","\u200C":"ZWNJ","\u200D":"ZWJ","\u2060":"WJ","\u180E":"MVS","\uFEFF":"BOM",\u3164:"HF",\uFFA0:"HHF"},N=/([\u200B-\u200D\u2060\u180E\uFEFF\u3164\uFFA0]|\uDB40[\uDC00-\uDC7F])/gu,k=16,L=(s,t={})=>{let r=new RegExp(N),n=r.exec(s);if(!n||t?.minSeverity==="CRITICAL")return[];let d=[],e=-1,i=-1,u=()=>{e=-1,i=-1},g=()=>{if(e===-1)return;let a=s.slice(e,i),o=z(a),c=[...a].map(h=>{let p=h.codePointAt(0);return v[h]||`U+${p?.toString(16).toUpperCase()}`}),l;if(o?l={ruleId:"PSU004",category:"INVISIBLE_CHAR",severity:"HIGH",message:"Unicode tag characters encode hidden ASCII content inside invisible text.",referenceUrl:"https://promptshield.js.org/docs/detectors/invisible-chars#PSU004",range:{start:e,end:i},offendingText:a,decodedPayload:o,readableLabel:"[TAG_PAYLOAD]",suggestion:"Remove Unicode tag characters containing hidden text."}:t.minSeverity!=="HIGH"&&a.length>=k?l={ruleId:"PSU005",category:"INVISIBLE_CHAR",severity:"MEDIUM",message:"Excessive invisible characters detected. Large invisible sequences are commonly used for padding or obfuscation.",referenceUrl:"https://promptshield.js.org/docs/detectors/invisible-chars#PSU005",range:{start:e,end:i},offendingText:a,readableLabel:`[${c.join(" ")}]`,suggestion:"Remove unnecessary invisible characters."}:t.minSeverity!=="MEDIUM"&&(l={ruleId:"PSU001",category:"INVISIBLE_CHAR",severity:"LOW",message:"Invisible Unicode characters detected. These characters can alter tokenization and prompt interpretation without being visible.",referenceUrl:"https://promptshield.js.org/docs/detectors/invisible-chars#PSU001",range:{start:e,end:i},offendingText:a,readableLabel:`[${c.join(" ")}]`,suggestion:"Remove invisible characters to ensure the prompt text is interpreted exactly as written."}),l&&(d.push(l),t.stopOnFirstThreat&&!t.ignoreChecker?.(l.range.start,l.range.end)))throw d;u()};try{for(;n!==null;){let a=n.index,o=n[0];if(a>0&&a<s.length-1){let c=s[a-1],l=s[a+o.length];if(c?.trim()&&l?.trim()){let h=a,p=a+o.length,m={ruleId:"PSU002",category:"INVISIBLE_CHAR",severity:"HIGH",message:"Invisible character detected inside a visible token. This can manipulate token boundaries or bypass validation.",referenceUrl:"https://promptshield.js.org/docs/detectors/invisible-chars#PSU002",range:{start:h,end:p},offendingText:o,readableLabel:`[${v[o]}]`||"[INVISIBLE]",suggestion:"Remove invisible characters embedded within words."};if(d.push(m),t.stopOnFirstThreat&&!t.ignoreChecker?.(h,p))return d}}e===-1?(e=a,i=a+o.length):a===i?i+=o.length:(g(),e=a,i=a+o.length),n=r.exec(s)}g()}catch(a){return a}return d},z=s=>{let t="",r=!1;for(let n of s){let d=n.codePointAt(0);if(d>=917504&&d<=917631){let e=d-917504;e>=32&&e<=126&&(t+=String.fromCharCode(e),r=!0)}}return r?t:void 0};var C=(s,t={})=>{if(t.minSeverity==="CRITICAL")return[];let r=[],n=s.normalize("NFKC");if(s===n)return[];let d=0,e=-1,i=-1,u=()=>{if(e===-1)return;let g=s.slice(e,i),a=g.normalize("NFKC"),o=/^[a-z0-9\s]+$/i.test(a),c=o?"PSN001":"PSN002",l=o?"LOW":"MEDIUM";r.push({ruleId:c,category:"NORMALIZATION",severity:l,message:"Text changes under Unicode NFKC normalization. This may cause ambiguity between displayed and interpreted content.",referenceUrl:`https://promptshield.js.org/docs/detectors/normalization#${c}`,range:{start:e,end:i},offendingText:g,decodedPayload:a,readableLabel:"[NFKC_DIFF]",suggestion:"Replace with normalized text to avoid ambiguity."}),e=-1,i=-1};for(let g of s){let a=g.normalize("NFKC");if(g!==a){if(e===-1?(e=d,i=d+g.length):i+=g.length,t.stopOnFirstThreat&&!t.ignoreChecker?.(d,d+g.length))return u(),r}else u();d+=g.length}return u(),r};var P=24,E=4096,_=/(?:[A-Za-z0-9+/_-]{4}[\s\u200B-\u200D\u2060\uFEFF]*){8,}(?:[A-Za-z0-9+/_-]{2}==|[A-Za-z0-9+/_-]{3}=)?/g,G=/\b(?:[0-9a-fA-F]{2}){12,}\b/g,B=/<!--[\s\S]*?-->/g,$=/\[\s*\]\([^)]+\)/g,K=/<(details|template)\b[^><]{0,200}>[\s\S]{0,100000}?<\/\1>/gi,X=/<summary\b[^><]{0,200}>[\s\S]{0,20000}?<\/summary>/i,V=/([\u200B-\u200D\u2060\uFEFF\u3164\uFFA0]+)/g,Y=s=>{if(typeof Buffer<"u")return Buffer.from(s,"base64").toString("utf8");let t=atob(s),r=Uint8Array.from(t,n=>n.charCodeAt(0));return new TextDecoder().decode(r)},Z=s=>{try{let t=s.replace(/\s+/g,"");if(t.length<P||t.length>E)return null;let r=Y(t);if(!r)return null;let n=0;for(let e of r){let i=e.charCodeAt(0);i>=32&&i<=126&&n++}return n/r.length>=.7?r:null}catch{return null}},q=s=>{try{if(s.length<P)return null;let t=new Uint8Array(s.length/2);for(let e=0;e<t.length;e++)t[e]=parseInt(s.slice(e*2,e*2+2),16);let r=new TextDecoder().decode(t),n=0;for(let e of r){let i=e.charCodeAt(0);i>=32&&i<=126&&n++}return n/r.length>=.7?r:null}catch{return null}},O=(s,t={})=>{if(t.minSeverity==="CRITICAL")return[];let r=[],n,d=new RegExp(V);for(;(n=d.exec(s))!==null;){let o=n[0];if(o.length<8||o.length>E)continue;let c=Array.from(new Set([...o]));if(c.length<2||c.length>4)continue;let l=[];for(let h=0;h<c.length;h++)for(let p=0;p<c.length;p++)h!==p&&l.push({zero:c[h],one:c[p]});for(let{zero:h,one:p}of l){let m="";for(let f of o)f===h?m+="0":f===p&&(m+="1");let y="";for(let f=0;f<m.length;f+=8){let b=m.slice(f,f+8);if(b.length!==8)continue;let S=parseInt(b,2);S>=32&&S<=126&&(y+=String.fromCharCode(S))}if(y.length>=3){let f=n.index,b=f+o.length;if(r.push({ruleId:"PSS001",category:"SMUGGLING",severity:"HIGH",message:"Detected hidden steganography message encoded in invisible characters.",range:{start:f,end:b},offendingText:o,decodedPayload:y,readableLabel:`[Hidden]: ${y.slice(0,120)}...`,suggestion:"Invisible-character encoding detected. Inspect hidden content.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS001"}),t.stopOnFirstThreat&&!t.ignoreChecker?.(f,b))return r;break}}}if(t.minSeverity==="HIGH")return r;let e=new RegExp(_);for(;(n=e.exec(s))!==null;){let o=n[0],c=Z(o);if(!c)continue;let l=n.index,h=l+o.length;if(r.push({ruleId:"PSS002",category:"SMUGGLING",severity:"MEDIUM",message:"Detected Base64 payload containing readable content.",range:{start:l,end:h},offendingText:o,decodedPayload:c,readableLabel:`[Base64]: ${c.slice(0,120)}...`,suggestion:"Decoded Base64 contains readable text. Inspect payload.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS002"}),t.stopOnFirstThreat&&!t.ignoreChecker?.(l,h))return r}let i=new RegExp(G);for(;(n=i.exec(s))!==null;){let o=n[0],c=q(o);if(!c)continue;let l=n.index,h=l+o.length;if(r.push({ruleId:"PSS006",category:"SMUGGLING",severity:"MEDIUM",message:"Detected hex-encoded payload containing readable content.",range:{start:l,end:h},offendingText:o,decodedPayload:c,readableLabel:`[HEX]: ${c.slice(0,120)}...`,suggestion:"Decoded hex contains readable text. Inspect payload.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS006"}),t.stopOnFirstThreat&&!t.ignoreChecker?.(l,h))return r}if(t.minSeverity==="MEDIUM")return r;let u=new RegExp(B);for(;(n=u.exec(s))!==null;){let o=n.index,c=o+n[0].length;if(r.push({ruleId:"PSS003",category:"SMUGGLING",severity:"LOW",message:"Detected hidden Markdown comment.",range:{start:o,end:c},offendingText:n[0],readableLabel:"[Hidden Comment]",suggestion:"Comments are not visible in rendered Markdown but can carry instructions.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS003"}),t.stopOnFirstThreat&&!t.ignoreChecker?.(o,c))return r}let g=new RegExp($);for(;(n=g.exec(s))!==null;){let o=n.index,c=o+n[0].length;if(r.push({ruleId:"PSS004",category:"SMUGGLING",severity:"LOW",message:"Detected empty Markdown link (invisible in rendered output).",range:{start:o,end:c},offendingText:n[0],readableLabel:"[Empty Link]",suggestion:"Empty links can be used to hide URLs or data.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS004"}),t.stopOnFirstThreat&&!t.ignoreChecker?.(o,c))return r}let a=new RegExp(K);for(;(n=a.exec(s))!==null;){let o=n[0];if(/^<details/i.test(o)&&X.test(o))continue;let c=n.index,l=c+o.length;if(r.push({ruleId:"PSS005",category:"SMUGGLING",severity:"LOW",message:"Detected hidden HTML container potentially concealing content.",range:{start:c,end:l},offendingText:o,readableLabel:"[Hidden HTML]",suggestion:"Hidden containers may conceal instructions from rendered output.",referenceUrl:"https://promptshield.js.org/docs/detectors/smuggling#PSS005"}),t.stopOnFirstThreat&&!t.ignoreChecker?.(c,l))return r}return r};var J={"\u202A":"PUSH","\u202B":"PUSH","\u202D":"PUSH","\u202E":"PUSH","\u2066":"PUSH","\u2067":"PUSH","\u2068":"PUSH","\u202C":"POP","\u2069":"POP","\u200E":"MARK","\u200F":"MARK","\u061C":"MARK"},A=(s,t={})=>{let r=[],n=[],d=0;for(let e=0;e<s.length;){let i=s.codePointAt(e),u=String.fromCodePoint(i),g=J[u];if(g==="PUSH")n.push(d);else if(g==="POP"&&n.length){let a=n.pop(),o=d+u.length,c=s.slice(a,o),l=s.slice(a+1,o-1);if(r.push({ruleId:"PST001",category:"TROJAN_SOURCE",severity:"CRITICAL",message:"Bidirectional override characters detected (Trojan Source). These characters can visually reorder text and mislead reviewers.",referenceUrl:"https://promptshield.js.org/docs/detectors/trojan-source#PST001",range:{start:a,end:o},offendingText:c,decodedPayload:l,readableLabel:"[BIDI_OVERRIDE]",suggestion:"Remove bidirectional control characters or replace them with visible equivalents."}),t.stopOnFirstThreat&&!t.ignoreChecker?.(a,o))return r}if((u===` | ||
| `||u==="\r")&&n.length){let a=n.pop(),o=d,c=s.slice(a,o),l=s.slice(a+1,o);if(r.push({ruleId:"PST002",category:"TROJAN_SOURCE",severity:"CRITICAL",message:"Unterminated bidirectional override sequence detected (Trojan Source). This may cause visual and logical text order to differ.",referenceUrl:"https://promptshield.js.org/docs/detectors/trojan-source#PST002",range:{start:a,end:o},offendingText:c,decodedPayload:l,readableLabel:"[BIDI_UNTERMINATED]",suggestion:"Ensure BIDI control characters are properly terminated within the same logical line or remove them entirely."}),t.stopOnFirstThreat&&!t.ignoreChecker?.(a,o))return r}d+=u.length,e+=u.length}for(;n.length;){let e=n.pop(),i=s.length,u=s.slice(e,i),g=s.slice(e+1);if(r.push({ruleId:"PST002",category:"TROJAN_SOURCE",severity:"CRITICAL",message:"Unterminated bidirectional override sequence detected (Trojan Source).",referenceUrl:"https://promptshield.js.org/docs/detectors/trojan-source#PST002",range:{start:e,end:i},offendingText:u,decodedPayload:g,readableLabel:"[BIDI_UNTERMINATED]",suggestion:"Remove or terminate bidirectional override characters before the end of the document."}),t.stopOnFirstThreat&&!t.ignoreChecker?.(e,i))return r}return r};var Q=s=>{let t=[0];for(let r=0;r<s.length;r++)s[r]===` | ||
| `&&t.push(r+1);return t},H=(s,t)=>{let{lineOffsets:r,baseLine:n,baseCol:d}=t,e=0,i=r.length-1;for(;e<=i;){let g=e+i>>1;r[g]<=s?e=g+1:i=g-1}let u=Math.max(i,0);return{line:n+u,column:s-r[u]+(u===0?d:1),index:s}},U=(s,t,r={})=>{let n=Q(t),{baseLine:d=1,baseCol:e=1}=r,i={lineOffsets:n,baseLine:d,baseCol:e};return s.map(u=>{let g=H(u.range.start,i),a=H(u.range.end,i);return{...u,range:{start:g,end:a}}})};var fe=(s,t={})=>{let r=[],n=[];t.disableTrojan||n.push(A),t.disableInvisible||n.push(L),t.disableHomoglyphs||n.push(x),t.disableNormalization||n.push(C),t.disableSmuggling||n.push(O),t.disableInjectionPatterns||n.push(R);for(let d of n){let e=d(s,t);if(r.push(...e),t.stopOnFirstThreat&&e.length>0)break}return{threats:U(r,s),isClean:r.length===0}};export{I as SEVERITY_MAP,T as ThreatCategory,z as decodeUnicodeTags,U as enrichWithLocation,Q as getLineOffsets,H as getLocForIndex,fe as scan,x as scanHomoglyphs,R as scanInjectionPatterns,L as scanInvisibleChars,C as scanNormalization,O as scanSmuggling,A as scanTrojanSource}; |
+3
-2
@@ -5,3 +5,3 @@ { | ||
| "private": false, | ||
| "version": "0.1.0", | ||
| "version": "1.0.0", | ||
| "description": "The heart of the PromptShield ecosystem. A zero-dependency, isomorphic TypeScript engine for detecting invisible characters, BIDI overrides, and homoglyph attacks in AI prompts.", | ||
@@ -45,3 +45,4 @@ "license": "MIT", | ||
| ], | ||
| "icon": "Shield" | ||
| "icon": "Shield", | ||
| "description": "High-performance threat detector" | ||
| }, | ||
@@ -48,0 +49,0 @@ "funding": [ |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
78218
37.48%730
46%2
-33.33%1
-50%