dsh-harbor-evolution 0.8.2 → 0.9.2

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.
@@ -0,0 +1,56 @@
1
+ const TOKEN_PATTERN = 'hctx_[A-Za-z0-9_-]{20,80}'
2
+
3
+ function escapeRegExp(value) {
4
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
5
+ }
6
+
7
+ function rawReferencePattern(token, global = false) {
8
+ const ref = token ? escapeRegExp(token) : TOKEN_PATTERN
9
+ return new RegExp(`@harbor(?:\\[[^\\]\\r\\n]{0,300}\\])?\\(${ref}\\)[ \\t]?`, global ? 'g' : '')
10
+ }
11
+
12
+ function occurrenceRanges(occurrences, token) {
13
+ return (Array.isArray(occurrences) ? occurrences : [])
14
+ .filter(item => item?.source === 'harbor' && (!token || item.ref === token))
15
+ .map(item => ({ start: Number(item.offset), end: Number(item.offset) + Number(item.length) }))
16
+ .filter(item => Number.isSafeInteger(item.start) && Number.isSafeInteger(item.end) && item.start >= 0 && item.end >= item.start)
17
+ .sort((left, right) => right.start - left.start)
18
+ }
19
+
20
+ /** Locate literal Harbor references that are not owned by another structured chip. */
21
+ export function rawHarborReferenceRanges(value, occurrences = [], token) {
22
+ const draft = String(value ?? '')
23
+ const occupied = (Array.isArray(occurrences) ? occurrences : [])
24
+ .map(item => ({ start: Number(item?.offset), end: Number(item?.offset) + Number(item?.length) }))
25
+ .filter(item => Number.isSafeInteger(item.start) && Number.isSafeInteger(item.end) && item.start >= 0 && item.end >= item.start)
26
+ return [...draft.matchAll(rawReferencePattern(token, true))]
27
+ .map(match => ({ start: match.index, end: match.index + match[0].length }))
28
+ .filter(range => !occupied.some(item => range.start < item.end && item.start < range.end))
29
+ .sort((left, right) => right.start - left.start)
30
+ }
31
+
32
+ /** Remove Harbor references while preserving all non-reference draft text. */
33
+ export function stripHarborReferences(value, occurrences = [], token) {
34
+ let draft = String(value ?? '')
35
+ const ranges = [...occurrenceRanges(occurrences, token), ...rawHarborReferenceRanges(draft, occurrences, token)]
36
+ .sort((left, right) => right.start - left.start)
37
+ for (const range of ranges) {
38
+ if (range.end > draft.length) continue
39
+ const end = draft[range.end] === ' ' ? range.end + 1 : range.end
40
+ draft = draft.slice(0, range.start) + draft.slice(end)
41
+ }
42
+ return draft
43
+ }
44
+
45
+ export function hasHarborReference(value, occurrences = [], token) {
46
+ if (!token) return false
47
+ if ((Array.isArray(occurrences) ? occurrences : []).some(item => item?.source === 'harbor' && item.ref === token)) return true
48
+ return rawHarborReferenceRanges(value, occurrences, token).length > 0
49
+ }
50
+
51
+ /** Enforce one explicit Harbor reference and retain the latest user-authored body. */
52
+ export function withHarborReference(value, reference, prompt = '', occurrences = []) {
53
+ const retained = stripHarborReferences(value, occurrences).replace(/^[ \t]+/, '')
54
+ const body = retained || String(prompt ?? '')
55
+ return `${reference}${body ? ` ${body}` : ''}`
56
+ }
@@ -0,0 +1,155 @@
1
+ const SENSITIVE_ASSIGNMENT_NAMES = [
2
+ 'authorization', 'cookie', 'cookies', 'token', 'auth[_-]?token',
3
+ 'access[_-]?token', 'refresh[_-]?token', 'session[_-]?token',
4
+ 'api[_-]?key', 'access[_-]?key', 'client[_-]?secret', 'secret',
5
+ 'secret[_-]?key', 'secret[_-]?access[_-]?key', 'private[_-]?key',
6
+ 'password', 'passwd',
7
+ ].join('|')
8
+
9
+ // Environment variables commonly namespace a sensitive suffix, for example
10
+ // OPENAI_API_KEY, MY_ACCESS_TOKEN, or AWS_SECRET_ACCESS_KEY. Keep each namespace
11
+ // segment unambiguous and bound the segment count: an unbounded repetition whose
12
+ // inner token could also consume "-" caused catastrophic backtracking on benign
13
+ // hyphenated text.
14
+ const SENSITIVE_ASSIGNMENT_KEY = `(?:[A-Za-z][A-Za-z0-9]*[_-]){0,16}(?:${SENSITIVE_ASSIGNMENT_NAMES})`
15
+ const ASSIGNMENT_VALUE = new RegExp(
16
+ `(^|[^A-Za-z0-9_])(["'\`]?)(${SENSITIVE_ASSIGNMENT_KEY})\\2\\s*[:=][^\\r\\n]*`,
17
+ 'gim',
18
+ )
19
+ const ASSIGNMENT_VALUE_TEST = new RegExp(
20
+ `(^|[^A-Za-z0-9_])(["'\`]?)(${SENSITIVE_ASSIGNMENT_KEY})\\2\\s*[:=]`,
21
+ 'im',
22
+ )
23
+ const QUOTED_AUTH_VALUE = /(["'`])(Bearer|Basic)\s+[^\r\n]*/gi
24
+ const AUTH_VALUE = /\b(Bearer|Basic)\s+[^\s,;"'`<>]+/gi
25
+ const AUTH_VALUE_TEST = /\b(?:Bearer|Basic)\s+(?:["'`]|[^\s,;"'`<>])/i
26
+ const URL_USERINFO_VALUE = /\b([A-Za-z][A-Za-z0-9+.-]{0,31}:\/\/)(?!\[REDACTED[^\]]*\]@)([^/\s?#@"'`<>]+)@/gi
27
+ const URL_USERINFO_VALUE_TEST = /\b[A-Za-z][A-Za-z0-9+.-]{0,31}:\/\/(?!\[REDACTED[^\]]*\]@)[^/\s?#@"'`<>]+@/i
28
+ const POSIX_LOCAL_PATH = /(?<![A-Za-z0-9:/])\/(?:[^/\r\n"'`<>,;]+\/)+[^/\r\n"'`<>,;]*|(?<![A-Za-z0-9:/])\/[A-Za-z0-9._~+-]+(?=$|[\s"'`<>,;])/g
29
+ const POSIX_LOCAL_PATH_TEST = /(?<![A-Za-z0-9:/])\/(?:[^/\r\n"'`<>,;]+\/)+[^/\r\n"'`<>,;]*|(?<![A-Za-z0-9:/])\/[A-Za-z0-9._~+-]+(?=$|[\s"'`<>,;])/
30
+ const WINDOWS_LOCAL_PATH = /(?:\b[A-Za-z]:\\|\\\\)[^\r\n"'`<>,;]*/g
31
+ const WINDOWS_LOCAL_PATH_TEST = /(?:\b[A-Za-z]:\\|\\\\)[^\r\n"'`<>,;]*/
32
+ const SENSITIVE_CONTAINER_KEY = /authorization|cookie|token|api[_-]?key|secret|password|request[_-]?headers/i
33
+ const SENSITIVE_CONTAINER_SUFFIXES = [
34
+ /headers?$/,
35
+ /headermaps?$/,
36
+ /env$/,
37
+ /env(?:vars?|maps?)$/,
38
+ /environment$/,
39
+ /environment(?:variables?|vars?|maps?)$/,
40
+ /credentials?$/,
41
+ /credentials?(?:maps?|stores?|values?)$/,
42
+ ]
43
+
44
+ const OPAQUE_SECRET_RULES = [
45
+ {
46
+ kind: 'pem',
47
+ // No footer is still sensitive: fail closed from the BEGIN line through
48
+ // the end of the string instead of returning a truncated private key.
49
+ pattern: /-----BEGIN [^-\r\n]{1,80}-----[\s\S]*?(?:-----END [^-\r\n]{1,80}-----|$)/gi,
50
+ },
51
+ {
52
+ kind: 'token',
53
+ pattern: /\b(?:sk|rk|pk)-(?:proj-)?[A-Za-z0-9_-]{12,}\b|\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b|\bxox[a-z]?-[A-Za-z0-9-]{10,}\b/gi,
54
+ },
55
+ {
56
+ kind: 'jwt',
57
+ pattern: /\beyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\b/g,
58
+ },
59
+ {
60
+ kind: 'aws',
61
+ pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g,
62
+ },
63
+ ]
64
+
65
+ function replacementFor(replacement, kind) {
66
+ return typeof replacement === 'function' ? replacement(kind) : replacement
67
+ }
68
+
69
+ export function isSensitiveCredentialContainerKey(value) {
70
+ const key = String(value ?? '')
71
+ if (SENSITIVE_CONTAINER_KEY.test(key)) return true
72
+ const canonical = key.replace(/[^A-Za-z0-9]/g, '').toLowerCase()
73
+ return SENSITIVE_CONTAINER_SUFFIXES.some(pattern => pattern.test(canonical))
74
+ }
75
+
76
+ export function redactCredentialTextWithCount(value, replacement = '[REDACTED]', preserveKey = true) {
77
+ let replacements = 0
78
+ let text = String(value ?? '').replace(
79
+ ASSIGNMENT_VALUE,
80
+ (_match, prefix, quote, key) => {
81
+ replacements += 1
82
+ return preserveKey ? `${prefix}${quote}${key}${quote}=${replacement}` : `${prefix}${replacement}`
83
+ },
84
+ )
85
+ text = text.replace(QUOTED_AUTH_VALUE, (_match, quote, scheme) => {
86
+ replacements += 1
87
+ return preserveKey ? `${quote}${scheme} ${replacement}${quote}` : replacement
88
+ })
89
+ text = text.replace(AUTH_VALUE, (_match, scheme) => {
90
+ replacements += 1
91
+ return preserveKey ? `${scheme} ${replacement}` : replacement
92
+ })
93
+ text = text.replace(URL_USERINFO_VALUE, (_match, scheme) => {
94
+ replacements += 1
95
+ return preserveKey ? `${scheme}${replacement}@` : scheme
96
+ })
97
+ return { text, replacements }
98
+ }
99
+
100
+ export function redactCredentialText(value, replacement = '[REDACTED]') {
101
+ return redactCredentialTextWithCount(value, replacement).text
102
+ }
103
+
104
+ export function containsCredentialText(value) {
105
+ const text = String(value ?? '')
106
+ return ASSIGNMENT_VALUE_TEST.test(text) || AUTH_VALUE_TEST.test(text) || URL_USERINFO_VALUE_TEST.test(text)
107
+ }
108
+
109
+ export function redactOpaqueSecretTextWithCount(value, replacement = '[REDACTED]') {
110
+ let text = String(value ?? '')
111
+ let replacements = 0
112
+ for (const rule of OPAQUE_SECRET_RULES) {
113
+ text = text.replace(rule.pattern, () => {
114
+ replacements += 1
115
+ return replacementFor(replacement, rule.kind)
116
+ })
117
+ }
118
+ return { text, replacements }
119
+ }
120
+
121
+ export function redactOpaqueSecretText(value, replacement = '[REDACTED]') {
122
+ return redactOpaqueSecretTextWithCount(value, replacement).text
123
+ }
124
+
125
+ export function containsOpaqueSecretText(value) {
126
+ const text = String(value ?? '')
127
+ return OPAQUE_SECRET_RULES.some(rule => {
128
+ rule.pattern.lastIndex = 0
129
+ const matched = rule.pattern.test(text)
130
+ rule.pattern.lastIndex = 0
131
+ return matched
132
+ })
133
+ }
134
+
135
+ export function redactLocalPathsWithCount(value, replacement = '[local path]') {
136
+ let replacements = 0
137
+ let text = String(value ?? '').replace(POSIX_LOCAL_PATH, () => {
138
+ replacements += 1
139
+ return replacement
140
+ })
141
+ text = text.replace(WINDOWS_LOCAL_PATH, () => {
142
+ replacements += 1
143
+ return replacement
144
+ })
145
+ return { text, replacements }
146
+ }
147
+
148
+ export function redactLocalPaths(value, replacement = '[local path]') {
149
+ return redactLocalPathsWithCount(value, replacement).text
150
+ }
151
+
152
+ export function containsLocalPath(value) {
153
+ const text = String(value ?? '')
154
+ return POSIX_LOCAL_PATH_TEST.test(text) || WINDOWS_LOCAL_PATH_TEST.test(text)
155
+ }