@ai-pip/csl 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/layers/csl/index.ts +1 -0
- package/layers/csl/src/adapters/index.ts +10 -0
- package/layers/csl/src/adapters/input/DOMAdapter.ts +236 -0
- package/layers/csl/src/adapters/input/UIAdapter.ts +0 -0
- package/layers/csl/src/adapters/output/ConsoleLogger.ts +34 -0
- package/layers/csl/src/adapters/output/CryptoHashGenerator.ts +29 -0
- package/layers/csl/src/adapters/output/FilePolicyRepository.ts +0 -0
- package/layers/csl/src/adapters/output/InMemoryPolicyRepository.ts +135 -0
- package/layers/csl/src/adapters/output/SystemTimestampProvider.ts +9 -0
- package/layers/csl/src/domain/entities/CSLResult.ts +309 -0
- package/layers/csl/src/domain/entities/Segment.ts +338 -0
- package/layers/csl/src/domain/entities/index.ts +2 -0
- package/layers/csl/src/domain/exceptions/ClassificationError.ts +26 -0
- package/layers/csl/src/domain/exceptions/SegmentationError.ts +30 -0
- package/layers/csl/src/domain/exceptions/index.ts +2 -0
- package/layers/csl/src/domain/index.ts +4 -0
- package/layers/csl/src/domain/services/AnomalyService.ts +255 -0
- package/layers/csl/src/domain/services/LineageService.ts +224 -0
- package/layers/csl/src/domain/services/NormalizationService.ts +392 -0
- package/layers/csl/src/domain/services/OriginClassificationService.ts +69 -0
- package/layers/csl/src/domain/services/PiDetectionService.ts +475 -0
- package/layers/csl/src/domain/services/PolicyService.ts +296 -0
- package/layers/csl/src/domain/services/SegmentClassificationService.ts +105 -0
- package/layers/csl/src/domain/services/SerializationService.ts +229 -0
- package/layers/csl/src/domain/services/index.ts +7 -0
- package/layers/csl/src/domain/value-objects/AnomalyScore.ts +23 -0
- package/layers/csl/src/domain/value-objects/ContentHash.ts +54 -0
- package/layers/csl/src/domain/value-objects/LineageEntry.ts +42 -0
- package/layers/csl/src/domain/value-objects/Origin-map.ts +67 -0
- package/layers/csl/src/domain/value-objects/Origin.ts +99 -0
- package/layers/csl/src/domain/value-objects/Pattern.ts +221 -0
- package/layers/csl/src/domain/value-objects/PiDetection.ts +140 -0
- package/layers/csl/src/domain/value-objects/PiDetectionResult.ts +275 -0
- package/layers/csl/src/domain/value-objects/PolicyRule.ts +151 -0
- package/layers/csl/src/domain/value-objects/TrustLevel.ts +34 -0
- package/layers/csl/src/domain/value-objects/index.ts +10 -0
- package/layers/csl/src/index.ts +7 -0
- package/layers/csl/src/ports/index.ts +10 -0
- package/layers/csl/src/ports/input/ClassificationPort.ts +76 -0
- package/layers/csl/src/ports/input/SegmentationPort.ts +81 -0
- package/layers/csl/src/ports/output/DOMAdapter.ts +14 -0
- package/layers/csl/src/ports/output/HashGenerator.ts +18 -0
- package/layers/csl/src/ports/output/Logger.ts +17 -0
- package/layers/csl/src/ports/output/PolicyRepository.ts +29 -0
- package/layers/csl/src/ports/output/SegmentClassified.ts +8 -0
- package/layers/csl/src/ports/output/TimeStampProvider.ts +5 -0
- package/layers/csl/src/services/CSLService.ts +393 -0
- package/layers/csl/src/services/index.ts +1 -0
- package/layers/csl/src/types/entities-types.ts +37 -0
- package/layers/csl/src/types/index.ts +4 -0
- package/layers/csl/src/types/pi-types.ts +111 -0
- package/layers/csl/src/types/port-output-types.ts +17 -0
- package/layers/csl/src/types/value-objects-types.ts +213 -0
- package/layers/csl/src/utils/colors.ts +25 -0
- package/layers/csl/src/utils/pattern-helpers.ts +174 -0
- package/package.json +4 -5
- package/src/index.ts +36 -36
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { CSLHashAlgorithm } from "../../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ContentHash value object represents a cryptographic hash of content
|
|
5
|
+
*
|
|
6
|
+
* @property value - The hash value as a hexadecimal string
|
|
7
|
+
* @property algorithm - The hash algorithm used (sha256 or sha512)
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const hash = new ContentHash('a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3', 'sha256')
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export class ContentHash {
|
|
15
|
+
readonly value: string
|
|
16
|
+
readonly algorithm: CSLHashAlgorithm
|
|
17
|
+
|
|
18
|
+
constructor(value: string, algorithm: CSLHashAlgorithm = 'sha256') {
|
|
19
|
+
if (!value || typeof value !== 'string') {
|
|
20
|
+
throw new Error('ContentHash value must be a non-empty string')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!['sha256', 'sha512'].includes(algorithm)) {
|
|
24
|
+
throw new Error(`Invalid HashAlgorithm: ${algorithm}. Must be 'sha256' or 'sha512'`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const hexPattern = /^[a-f0-9]+$/i
|
|
28
|
+
if (!hexPattern.test(value)) {
|
|
29
|
+
throw new Error('ContentHash value must be a valid hexadecimal string')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const expectedLength = algorithm === 'sha256' ? 64 : 128
|
|
33
|
+
if (value.length !== expectedLength) {
|
|
34
|
+
throw new Error(`ContentHash value length must be ${expectedLength} characters for ${algorithm}`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.value = value.toLowerCase()
|
|
38
|
+
this.algorithm = algorithm
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns true if the hash uses SHA-256 algorithm
|
|
43
|
+
*/
|
|
44
|
+
isSha256(): boolean {
|
|
45
|
+
return this.algorithm === 'sha256'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns true if the hash uses SHA-512 algorithm
|
|
50
|
+
*/
|
|
51
|
+
isSha512(): boolean {
|
|
52
|
+
return this.algorithm === 'sha512'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LineageEntry value object represents a single entry in the lineage chain of a content segment
|
|
3
|
+
*
|
|
4
|
+
* @property step - Description of the processing step
|
|
5
|
+
* @property timestamp - Unix timestamp in milliseconds
|
|
6
|
+
* @property notes - Optional additional notes about this step
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const entry = new LineageEntry('normalization', Date.now(), 'Unicode normalization applied')
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export class LineageEntry {
|
|
14
|
+
readonly step: string
|
|
15
|
+
readonly timestamp: number
|
|
16
|
+
readonly notes?: string
|
|
17
|
+
|
|
18
|
+
constructor(step: string, timestamp: number, notes?: string) {
|
|
19
|
+
if (!step || typeof step !== 'string' || step.trim().length === 0) {
|
|
20
|
+
throw new Error('LineageEntry step must be a non-empty string')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (typeof timestamp !== 'number' || timestamp < 0 || !Number.isFinite(timestamp)) {
|
|
24
|
+
throw new Error('LineageEntry timestamp must be a valid positive number')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (notes !== undefined && (typeof notes !== 'string' || notes.trim().length === 0)) {
|
|
28
|
+
throw new Error('LineageEntry notes must be a non-empty string if provided')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.step = step.trim()
|
|
32
|
+
this.timestamp = timestamp
|
|
33
|
+
this.notes = notes?.trim() ?? ''
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns true if this entry has notes
|
|
38
|
+
*/
|
|
39
|
+
hasNotes(): boolean {
|
|
40
|
+
return this.notes !== undefined && this.notes.length > 0
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { OriginType, TrustLevelType } from "../../types";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* originMap is the deterministic mapping from OriginType to TrustLevelType.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* This map defines the **deterministic classification rules** for content segments.
|
|
10
|
+
* The mapping is based solely on the origin type, not on content analysis.
|
|
11
|
+
*
|
|
12
|
+
* **Key Principles:**
|
|
13
|
+
* - 100% deterministic: same origin → same trust level, always
|
|
14
|
+
* - No heuristics or content analysis
|
|
15
|
+
* - All OriginType values must be present in this map
|
|
16
|
+
* - Future content analysis can be added as a separate layer
|
|
17
|
+
*
|
|
18
|
+
* **Mapping Rules:**
|
|
19
|
+
* - USER → UC (always untrusted for security)
|
|
20
|
+
* - DOM_VISIBLE → STC (user can verify)
|
|
21
|
+
* - DOM_HIDDEN → UC (potential attack vector)
|
|
22
|
+
* - DOM_ATTRIBUTE → STC (visible in source)
|
|
23
|
+
* - SCRIPT_INJECTED → UC (can be manipulated)
|
|
24
|
+
* - NETWORK_FETCHED → UC (external source)
|
|
25
|
+
* - SYSTEM_GENERATED → TC (system controls it)
|
|
26
|
+
* - UNKNOWN → UC (unknown is untrusted by default)
|
|
27
|
+
*/
|
|
28
|
+
export const originMap = new Map<OriginType, TrustLevelType>([
|
|
29
|
+
// User origins - always untrusted (security by default)
|
|
30
|
+
[OriginType.USER, TrustLevelType.UC],
|
|
31
|
+
|
|
32
|
+
// DOM origins - trust based on visibility
|
|
33
|
+
[OriginType.DOM_VISIBLE, TrustLevelType.STC],
|
|
34
|
+
[OriginType.DOM_HIDDEN, TrustLevelType.UC],
|
|
35
|
+
[OriginType.DOM_ATTRIBUTE, TrustLevelType.STC],
|
|
36
|
+
|
|
37
|
+
// External origins - always untrusted
|
|
38
|
+
[OriginType.SCRIPT_INJECTED, TrustLevelType.UC],
|
|
39
|
+
[OriginType.NETWORK_FETCHED, TrustLevelType.UC],
|
|
40
|
+
|
|
41
|
+
// System origins - trusted (system controls)
|
|
42
|
+
[OriginType.SYSTEM_GENERATED, TrustLevelType.TC],
|
|
43
|
+
|
|
44
|
+
// Unknown - untrusted by default (fail-secure)
|
|
45
|
+
[OriginType.UNKNOWN, TrustLevelType.UC],
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validates that all OriginType values are mapped in originMap.
|
|
50
|
+
*
|
|
51
|
+
* @remarks
|
|
52
|
+
* This function ensures completeness of the mapping at runtime.
|
|
53
|
+
* Throws an error if any OriginType is missing from the map.
|
|
54
|
+
*
|
|
55
|
+
* @throws {Error} If any OriginType is not present in originMap
|
|
56
|
+
*/
|
|
57
|
+
export function validateOriginMap(): void {
|
|
58
|
+
const allOriginTypes = Object.values(OriginType);
|
|
59
|
+
const missingTypes = allOriginTypes.filter(type => !originMap.has(type));
|
|
60
|
+
|
|
61
|
+
if (missingTypes.length > 0) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Missing origin mappings: ${missingTypes.join(', ')}. ` +
|
|
64
|
+
`All OriginType values must be mapped in originMap.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { OriginType } from '../../types'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Origin value object represents the deterministic source of a content segment.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* Origin is a value object that encapsulates the source type of content.
|
|
9
|
+
* The origin type determines the trust level through deterministic classification.
|
|
10
|
+
*
|
|
11
|
+
* **Deterministic Classification:**
|
|
12
|
+
* - Classification is based solely on the origin type
|
|
13
|
+
* - No content analysis or heuristics
|
|
14
|
+
* - 100% reproducible: same origin → same trust level
|
|
15
|
+
*
|
|
16
|
+
* @property type - The origin type (USER, DOM_VISIBLE, DOM_HIDDEN, etc.)
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* // Create origin for user input
|
|
21
|
+
* const userOrigin = new Origin(OriginType.USER)
|
|
22
|
+
*
|
|
23
|
+
* // Check origin characteristics
|
|
24
|
+
* if (userOrigin.isUser()) {
|
|
25
|
+
* // Handle user input with extra security
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* // Create origin for system content
|
|
29
|
+
* const systemOrigin = new Origin(OriginType.SYSTEM_GENERATED)
|
|
30
|
+
* if (systemOrigin.isSystem()) {
|
|
31
|
+
* // Handle system content with trust
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
export class Origin {
|
|
38
|
+
readonly type: OriginType;
|
|
39
|
+
|
|
40
|
+
constructor(type: OriginType){
|
|
41
|
+
if(!Object.values(OriginType).includes(type)){
|
|
42
|
+
throw new Error(`Invalid Origin type: ${type}`)
|
|
43
|
+
}
|
|
44
|
+
this.type = type
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Checks if the origin is from DOM (visible, hidden, or attribute)
|
|
49
|
+
*/
|
|
50
|
+
isDom(): boolean {
|
|
51
|
+
return this.type === OriginType.DOM_HIDDEN ||
|
|
52
|
+
this.type === OriginType.DOM_VISIBLE ||
|
|
53
|
+
this.type === OriginType.DOM_ATTRIBUTE
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Checks if the origin is direct user input
|
|
58
|
+
*/
|
|
59
|
+
isUser(): boolean {
|
|
60
|
+
return this.type === OriginType.USER
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Checks if the origin is system-generated content
|
|
65
|
+
*/
|
|
66
|
+
isSystem(): boolean {
|
|
67
|
+
return this.type === OriginType.SYSTEM_GENERATED
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Checks if the origin is script-injected content
|
|
72
|
+
*/
|
|
73
|
+
isInjected(): boolean {
|
|
74
|
+
return this.type === OriginType.SCRIPT_INJECTED
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Checks if the origin is unknown
|
|
79
|
+
*/
|
|
80
|
+
isUnknown(): boolean {
|
|
81
|
+
return this.type === OriginType.UNKNOWN
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Checks if the origin is network-fetched content
|
|
86
|
+
*/
|
|
87
|
+
isNetworkFetched(): boolean {
|
|
88
|
+
return this.type === OriginType.NETWORK_FETCHED
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Checks if the origin is from an external source (network or injected)
|
|
93
|
+
*/
|
|
94
|
+
isExternal(): boolean {
|
|
95
|
+
return this.type === OriginType.NETWORK_FETCHED ||
|
|
96
|
+
this.type === OriginType.SCRIPT_INJECTED
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { RiskScore } from '../../types'
|
|
2
|
+
import { PatternHelpers } from '../../utils/pattern-helpers'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pattern value object represents a detection pattern for prompt injection attacks
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* Patterns define how to detect specific types of prompt injection attempts.
|
|
9
|
+
* They can be created from regular expression strings or RegExp objects.
|
|
10
|
+
* The pattern is normalized to a RegExp internally for consistent matching.
|
|
11
|
+
*
|
|
12
|
+
* **Key Features:**
|
|
13
|
+
* - Normalizes regex strings to RegExp objects
|
|
14
|
+
* - Maintains immutability through Object.freeze
|
|
15
|
+
* - Provides methods for pattern matching and match information extraction
|
|
16
|
+
* - Validates all inputs to ensure pattern integrity
|
|
17
|
+
*
|
|
18
|
+
* **Usage:**
|
|
19
|
+
* Patterns are typically used by PiDetectionService to scan content for malicious patterns.
|
|
20
|
+
* Each pattern has a base confidence score that can be adjusted based on context.
|
|
21
|
+
*
|
|
22
|
+
* @property pattern_type - Type of pattern (e.g., "role_swapping", "instruction_override")
|
|
23
|
+
* @property regex - Compiled regular expression for pattern matching (always RegExp after construction)
|
|
24
|
+
* @property base_confidence - Base confidence score for this pattern (0-1)
|
|
25
|
+
* @property description - Optional description of what this pattern detects
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* // Create pattern from string
|
|
30
|
+
* const pattern = new Pattern(
|
|
31
|
+
* 'role_swapping',
|
|
32
|
+
* 'you are no longer',
|
|
33
|
+
* 0.9,
|
|
34
|
+
* 'Detects attempts to change AI role'
|
|
35
|
+
* )
|
|
36
|
+
*
|
|
37
|
+
* // Create pattern from RegExp
|
|
38
|
+
* const pattern2 = new Pattern(
|
|
39
|
+
* 'instruction_override',
|
|
40
|
+
* /ignore\s+(all\s+)?(previous|prior)\s+instructions?/i,
|
|
41
|
+
* 0.95
|
|
42
|
+
* )
|
|
43
|
+
*
|
|
44
|
+
* // Check if pattern matches content
|
|
45
|
+
* if (pattern.matches(content)) {
|
|
46
|
+
* const match = pattern.findMatch(content)
|
|
47
|
+
* if (match) {
|
|
48
|
+
* // Use match information to create PiDetection
|
|
49
|
+
* }
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export class Pattern {
|
|
54
|
+
readonly pattern_type: string
|
|
55
|
+
readonly regex: RegExp
|
|
56
|
+
readonly base_confidence: RiskScore
|
|
57
|
+
readonly description: string
|
|
58
|
+
|
|
59
|
+
constructor(
|
|
60
|
+
pattern_type: string,
|
|
61
|
+
regex: string | RegExp,
|
|
62
|
+
base_confidence: RiskScore,
|
|
63
|
+
description?: string
|
|
64
|
+
) {
|
|
65
|
+
// Validate all inputs using helper functions
|
|
66
|
+
PatternHelpers.validatePatternType(pattern_type)
|
|
67
|
+
PatternHelpers.validateRegex(regex)
|
|
68
|
+
PatternHelpers.validateBaseConfidence(base_confidence)
|
|
69
|
+
PatternHelpers.validateDescription(description)
|
|
70
|
+
|
|
71
|
+
this.pattern_type = pattern_type.trim()
|
|
72
|
+
|
|
73
|
+
// Validate and process regex
|
|
74
|
+
const regexSource = typeof regex === 'string' ? regex : regex.source
|
|
75
|
+
PatternHelpers.validateRegexLength(regexSource)
|
|
76
|
+
PatternHelpers.checkForReDoSPatterns(regexSource, this.pattern_type)
|
|
77
|
+
|
|
78
|
+
// Normalize regex to RegExp for consistency and immutability
|
|
79
|
+
this.regex = typeof regex === 'string'
|
|
80
|
+
? PatternHelpers.compileRegexString(regex)
|
|
81
|
+
: PatternHelpers.cloneRegExp(regex)
|
|
82
|
+
|
|
83
|
+
this.base_confidence = base_confidence
|
|
84
|
+
this.description = description?.trim() ?? ''
|
|
85
|
+
|
|
86
|
+
// Freeze the object for immutability
|
|
87
|
+
Object.freeze(this)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Checks if the pattern matches the given content
|
|
92
|
+
*
|
|
93
|
+
* @param content - The content string to test against the pattern
|
|
94
|
+
* @returns true if the pattern matches, false otherwise
|
|
95
|
+
*
|
|
96
|
+
* @throws {TypeError} If content is not a valid non-empty string
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* const pattern = new Pattern('role_swapping', 'you are no longer', 0.9)
|
|
101
|
+
* if (pattern.matches('Hello, you are no longer an AI')) {
|
|
102
|
+
* // Pattern detected
|
|
103
|
+
* }
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
matches(content: string): boolean {
|
|
107
|
+
PatternHelpers.validateContent(content, 'matches')
|
|
108
|
+
PatternHelpers.validateContentLength(content, 'matches')
|
|
109
|
+
return this.regex.test(content)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Finds the first match in content and returns match information
|
|
114
|
+
*
|
|
115
|
+
* @remarks
|
|
116
|
+
* This method is useful when you need to create a PiDetection from a pattern match.
|
|
117
|
+
* It returns the exact matched text and its position in the content.
|
|
118
|
+
*
|
|
119
|
+
* @param content - The content string to search for matches
|
|
120
|
+
* @returns Match information with matched text and position, or null if no match found
|
|
121
|
+
*
|
|
122
|
+
* @throws {TypeError} If content is not a valid non-empty string
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* const pattern = new Pattern('role_swapping', 'you are no longer', 0.9)
|
|
127
|
+
* const match = pattern.findMatch('Hello, you are no longer an AI assistant')
|
|
128
|
+
*
|
|
129
|
+
* if (match) {
|
|
130
|
+
* // match.matched = 'you are no longer'
|
|
131
|
+
* // match.position = { start: 7, end: 25 }
|
|
132
|
+
* const detection = new PiDetection(
|
|
133
|
+
* pattern.pattern_type,
|
|
134
|
+
* match.matched,
|
|
135
|
+
* match.position,
|
|
136
|
+
* pattern.base_confidence
|
|
137
|
+
* )
|
|
138
|
+
* }
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
findMatch(content: string): { matched: string; position: { start: number; end: number } } | null {
|
|
142
|
+
PatternHelpers.validateContent(content, 'findMatch')
|
|
143
|
+
PatternHelpers.validateContentLength(content, 'findMatch')
|
|
144
|
+
|
|
145
|
+
const match = this.regex.exec(content)
|
|
146
|
+
if (!match) {
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return PatternHelpers.createMatchResult(match)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Finds all matches in content and returns match information for each
|
|
155
|
+
*
|
|
156
|
+
* @remarks
|
|
157
|
+
* This method finds all non-overlapping matches of the pattern in the content.
|
|
158
|
+
* Useful when a pattern might appear multiple times in the same content.
|
|
159
|
+
*
|
|
160
|
+
* @param content - The content string to search for matches
|
|
161
|
+
* @returns Array of match information, empty array if no matches found
|
|
162
|
+
*
|
|
163
|
+
* @throws {TypeError} If content is not a valid non-empty string
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```typescript
|
|
167
|
+
* const pattern = new Pattern('instruction_override', 'ignore', 0.9)
|
|
168
|
+
* const matches = pattern.findAllMatches('ignore this and ignore that')
|
|
169
|
+
* // Returns: [
|
|
170
|
+
* // { matched: 'ignore', position: { start: 0, end: 6 } },
|
|
171
|
+
* // { matched: 'ignore', position: { start: 20, end: 26 } }
|
|
172
|
+
* // ]
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
findAllMatches(content: string): Array<{ matched: string; position: { start: number; end: number } }> {
|
|
176
|
+
PatternHelpers.validateContent(content, 'findAllMatches')
|
|
177
|
+
PatternHelpers.validateContentLength(content, 'findAllMatches')
|
|
178
|
+
|
|
179
|
+
const matches: Array<{ matched: string; position: { start: number; end: number } }> = []
|
|
180
|
+
const globalRegex = new RegExp(this.regex.source, this.regex.flags + 'g')
|
|
181
|
+
let match: RegExpExecArray | null
|
|
182
|
+
let iterations = 0
|
|
183
|
+
|
|
184
|
+
while ((match = globalRegex.exec(content)) !== null) {
|
|
185
|
+
iterations++
|
|
186
|
+
|
|
187
|
+
if (PatternHelpers.checkMatchLimits(matches.length, iterations)) {
|
|
188
|
+
break
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
matches.push(PatternHelpers.createMatchResult(match))
|
|
192
|
+
PatternHelpers.handleEmptyStringMatch(globalRegex, match)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return matches
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Returns the base confidence score for this pattern
|
|
200
|
+
*
|
|
201
|
+
* @returns The base confidence score (0-1)
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```typescript
|
|
205
|
+
* const pattern = new Pattern('role_swapping', 'you are no longer', 0.9)
|
|
206
|
+
* const confidence = pattern.getConfidence() // Returns: 0.9
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
getConfidence(): RiskScore {
|
|
210
|
+
return this.base_confidence
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Returns true if this pattern has a description
|
|
215
|
+
*
|
|
216
|
+
* @returns true if description is not empty, false otherwise
|
|
217
|
+
*/
|
|
218
|
+
hasDescription(): boolean {
|
|
219
|
+
return this.description.length > 0
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { RiskScore } from '../../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Position represents the location of a detected pattern within content
|
|
5
|
+
*
|
|
6
|
+
* @property start - Start position (inclusive, 0-based index)
|
|
7
|
+
* @property end - End position (exclusive, 0-based index)
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const position = { start: 12, end: 35 }
|
|
12
|
+
* // This means the pattern spans from character 12 to 34 (inclusive)
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export interface Position {
|
|
16
|
+
readonly start: number
|
|
17
|
+
readonly end: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* PiDetection value object represents a single prompt injection detection
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* This value object encapsulates all information about a single detected pattern:
|
|
25
|
+
* - What type of pattern was detected
|
|
26
|
+
* - The exact text that matched
|
|
27
|
+
* - Where it was found in the content
|
|
28
|
+
* - How confident the detection is
|
|
29
|
+
*
|
|
30
|
+
* **Immutability:**
|
|
31
|
+
* All properties are readonly and the object is frozen to ensure immutability.
|
|
32
|
+
*
|
|
33
|
+
* @property pattern_type - Type of malicious pattern detected (e.g., "role_swapping", "instruction_override")
|
|
34
|
+
* @property matched_pattern - The exact text that matched the pattern
|
|
35
|
+
* @property position - Location of the pattern in the content (start and end indices)
|
|
36
|
+
* @property confidence - Confidence score for this specific detection (0-1)
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* const detection = new PiDetection(
|
|
41
|
+
* 'role_swapping',
|
|
42
|
+
* 'you are no longer an AI assistant',
|
|
43
|
+
* { start: 12, end: 47 },
|
|
44
|
+
* 0.95
|
|
45
|
+
* )
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export class PiDetection {
|
|
49
|
+
readonly pattern_type: string
|
|
50
|
+
readonly matched_pattern: string
|
|
51
|
+
readonly position: Position
|
|
52
|
+
readonly confidence: RiskScore
|
|
53
|
+
|
|
54
|
+
constructor(
|
|
55
|
+
pattern_type: string,
|
|
56
|
+
matched_pattern: string,
|
|
57
|
+
position: Position,
|
|
58
|
+
confidence: RiskScore
|
|
59
|
+
) {
|
|
60
|
+
// Validate pattern_type
|
|
61
|
+
if (!pattern_type || typeof pattern_type !== 'string' || pattern_type.trim().length === 0) {
|
|
62
|
+
throw new Error('PiDetection pattern_type must be a non-empty string')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Validate matched_pattern
|
|
66
|
+
if (!matched_pattern || typeof matched_pattern !== 'string') {
|
|
67
|
+
throw new Error('PiDetection matched_pattern must be a non-empty string')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Validate position
|
|
71
|
+
if (!position || typeof position !== 'object') {
|
|
72
|
+
throw new TypeError('PiDetection position must be an object with start and end properties')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof position.start !== 'number' || !Number.isFinite(position.start) || position.start < 0) {
|
|
76
|
+
throw new Error('PiDetection position.start must be a valid non-negative number')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof position.end !== 'number' || !Number.isFinite(position.end) || position.end < 0) {
|
|
80
|
+
throw new Error('PiDetection position.end must be a valid non-negative number')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (position.end <= position.start) {
|
|
84
|
+
throw new Error('PiDetection position.end must be greater than position.start')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Validate confidence
|
|
88
|
+
if (typeof confidence !== 'number' || !Number.isFinite(confidence)) {
|
|
89
|
+
throw new TypeError('PiDetection confidence must be a valid number')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (confidence < 0 || confidence > 1) {
|
|
93
|
+
throw new Error('PiDetection confidence must be between 0 and 1')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Validate that matched_pattern length matches position
|
|
97
|
+
const patternLength = position.end - position.start
|
|
98
|
+
if (matched_pattern.length !== patternLength) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`PiDetection matched_pattern length (${matched_pattern.length}) does not match position range (${patternLength})`
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.pattern_type = pattern_type.trim()
|
|
105
|
+
this.matched_pattern = matched_pattern
|
|
106
|
+
this.position = Object.freeze({ ...position })
|
|
107
|
+
this.confidence = confidence
|
|
108
|
+
|
|
109
|
+
// Freeze the entire object for immutability
|
|
110
|
+
Object.freeze(this)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Returns the length of the detected pattern
|
|
115
|
+
*/
|
|
116
|
+
getLength(): number {
|
|
117
|
+
return this.position.end - this.position.start
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Returns true if this detection has high confidence (>= 0.7)
|
|
122
|
+
*/
|
|
123
|
+
isHighConfidence(): boolean {
|
|
124
|
+
return this.confidence >= 0.7
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Returns true if this detection has medium confidence (>= 0.3 and < 0.7)
|
|
129
|
+
*/
|
|
130
|
+
isMediumConfidence(): boolean {
|
|
131
|
+
return this.confidence >= 0.3 && this.confidence < 0.7
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Returns true if this detection has low confidence (< 0.3)
|
|
136
|
+
*/
|
|
137
|
+
isLowConfidence(): boolean {
|
|
138
|
+
return this.confidence < 0.3
|
|
139
|
+
}
|
|
140
|
+
}
|