securityclaw 0.0.1

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +135 -0
  4. package/README.zh-CN.md +135 -0
  5. package/admin/public/app.js +148 -0
  6. package/admin/public/favicon.svg +21 -0
  7. package/admin/public/index.html +31 -0
  8. package/admin/public/styles.css +2715 -0
  9. package/admin/server.ts +1053 -0
  10. package/bin/install-lib.mjs +88 -0
  11. package/bin/securityclaw.mjs +66 -0
  12. package/config/policy.default.yaml +520 -0
  13. package/index.ts +2662 -0
  14. package/install.sh +22 -0
  15. package/openclaw.plugin.json +60 -0
  16. package/package.json +69 -0
  17. package/src/admin/build.ts +113 -0
  18. package/src/admin/console_notice.ts +195 -0
  19. package/src/admin/dashboard_url_state.ts +80 -0
  20. package/src/admin/openclaw_session_catalog.ts +137 -0
  21. package/src/admin/runtime_guard.ts +51 -0
  22. package/src/admin/skill_interception_store.ts +1606 -0
  23. package/src/application/commands/approval_commands.ts +189 -0
  24. package/src/approvals/chat_approval_store.ts +433 -0
  25. package/src/config/live_config.ts +144 -0
  26. package/src/config/loader.ts +168 -0
  27. package/src/config/runtime_override.ts +66 -0
  28. package/src/config/strategy_store.ts +121 -0
  29. package/src/config/validator.ts +222 -0
  30. package/src/domain/models/resource_context.ts +31 -0
  31. package/src/domain/ports/approval_repository.ts +40 -0
  32. package/src/domain/ports/notification_port.ts +29 -0
  33. package/src/domain/ports/openclaw_adapter.ts +22 -0
  34. package/src/domain/services/account_policy_engine.ts +163 -0
  35. package/src/domain/services/approval_service.ts +336 -0
  36. package/src/domain/services/approval_subject_resolver.ts +37 -0
  37. package/src/domain/services/context_inference_service.ts +502 -0
  38. package/src/domain/services/file_rule_registry.ts +171 -0
  39. package/src/domain/services/formatting_service.ts +101 -0
  40. package/src/domain/services/path_candidate_inference.ts +111 -0
  41. package/src/domain/services/sensitive_path_registry.ts +288 -0
  42. package/src/domain/services/sensitivity_label_inference.ts +161 -0
  43. package/src/domain/services/shell_filesystem_inference.ts +360 -0
  44. package/src/engine/approval_fsm.ts +104 -0
  45. package/src/engine/decision_engine.ts +39 -0
  46. package/src/engine/dlp_engine.ts +91 -0
  47. package/src/engine/rule_engine.ts +208 -0
  48. package/src/events/emitter.ts +86 -0
  49. package/src/events/schema.ts +27 -0
  50. package/src/hooks/context_guard.ts +36 -0
  51. package/src/hooks/output_guard.ts +66 -0
  52. package/src/hooks/persist_guard.ts +69 -0
  53. package/src/hooks/policy_guard.ts +222 -0
  54. package/src/hooks/result_guard.ts +88 -0
  55. package/src/i18n/locale.ts +36 -0
  56. package/src/index.ts +255 -0
  57. package/src/infrastructure/adapters/notification_adapter.ts +173 -0
  58. package/src/infrastructure/adapters/openclaw_adapter_impl.ts +59 -0
  59. package/src/infrastructure/config/plugin_config_parser.ts +105 -0
  60. package/src/monitoring/status_store.ts +612 -0
  61. package/src/types.ts +409 -0
  62. package/src/utils.ts +97 -0
@@ -0,0 +1,101 @@
1
+ import type { ResourceScope } from "../../types.ts";
2
+ import type { SecurityClawLocale } from "../../i18n/locale.ts";
3
+ import { pickLocalized } from "../../i18n/locale.ts";
4
+
5
+ export class FormattingService {
6
+ static summarizeForLog(value: unknown, maxLength: number): string {
7
+ try {
8
+ const text = JSON.stringify(value);
9
+ if (text === undefined) {
10
+ return String(value);
11
+ }
12
+ if (text.length <= maxLength) {
13
+ return text;
14
+ }
15
+ return `${text.slice(0, maxLength)}...(truncated)`;
16
+ } catch {
17
+ return "[unserializable]";
18
+ }
19
+ }
20
+
21
+ static formatToolBlockReason(
22
+ toolName: string,
23
+ scope: string,
24
+ traceId: string,
25
+ decision: "challenge" | "block",
26
+ decisionSource: string,
27
+ resourceScope: ResourceScope,
28
+ reasonCodes: string[],
29
+ rules: string,
30
+ locale: SecurityClawLocale = "en",
31
+ ): string {
32
+ const reasons = reasonCodes.join(", ");
33
+ const resourceLabel = FormattingService.formatResourceScopeLabel(resourceScope, locale);
34
+ const lines = [
35
+ pickLocalized(
36
+ locale,
37
+ decision === "challenge" ? "SecurityClaw 需要审批" : "SecurityClaw 已阻止此操作",
38
+ decision === "challenge" ? "SecurityClaw Approval Required" : "SecurityClaw Blocked",
39
+ ),
40
+ `${pickLocalized(locale, "工具", "Tool")}: ${toolName}`,
41
+ `${pickLocalized(locale, "范围", "Scope")}: ${scope}`,
42
+ `${pickLocalized(locale, "资源", "Resource")}: ${resourceLabel} (${resourceScope})`,
43
+ `${pickLocalized(locale, "来源", "Source")}: ${decisionSource}`,
44
+ `${pickLocalized(locale, "原因", "Reason")}: ${reasons || pickLocalized(locale, "策略要求复核", "Policy review required")}`,
45
+ ...(rules && rules !== "-" ? [`${pickLocalized(locale, "规则", "Policy")}: ${rules}`] : []),
46
+ `${pickLocalized(
47
+ locale,
48
+ "处理",
49
+ "Action",
50
+ )}: ${pickLocalized(
51
+ locale,
52
+ decision === "challenge" ? "联系管理员审批后重试" : "联系安全管理员调整策略",
53
+ decision === "challenge" ? "Contact an admin to approve and retry" : "Contact a security admin to adjust policy",
54
+ )}`,
55
+ `${pickLocalized(locale, "追踪", "Trace")}: ${traceId}`,
56
+ ];
57
+ return lines.join("\n");
58
+ }
59
+
60
+ private static formatResourceScopeLabel(scope: ResourceScope, locale: SecurityClawLocale): string {
61
+ if (scope === "workspace_inside") {
62
+ return pickLocalized(locale, "工作区内", "Inside workspace");
63
+ }
64
+ if (scope === "workspace_outside") {
65
+ return pickLocalized(locale, "工作区外", "Outside workspace");
66
+ }
67
+ if (scope === "system") {
68
+ return pickLocalized(locale, "系统目录", "System directory");
69
+ }
70
+ return pickLocalized(locale, "无路径", "No path");
71
+ }
72
+
73
+ static normalizeToolName(rawToolName: string): string {
74
+ const tool = rawToolName.trim().toLowerCase();
75
+ if (tool === "exec" || tool === "shell" || tool === "shell_exec") {
76
+ return "shell.exec";
77
+ }
78
+ if (tool === "fs.list" || tool === "file.list") {
79
+ return "filesystem.list";
80
+ }
81
+ return rawToolName;
82
+ }
83
+
84
+ static matchedRuleIds(matches: Array<{ rule: { rule_id: string } }>): string {
85
+ if (matches.length === 0) {
86
+ return "-";
87
+ }
88
+ return matches.map((match) => match.rule.rule_id).join(",");
89
+ }
90
+
91
+ static findingsToText(findings: Array<{ pattern_name: string; path: string }>): string {
92
+ return findings.map((finding) => `${finding.pattern_name}@${finding.path}`).join(", ");
93
+ }
94
+
95
+ static resolveScope(ctx: { workspaceDir?: string; channelId?: string }): string {
96
+ if (ctx.workspaceDir) {
97
+ return ctx.workspaceDir.split("/").pop() || "default";
98
+ }
99
+ return ctx.channelId ?? "default";
100
+ }
101
+ }
@@ -0,0 +1,111 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+
4
+ const HOME_DIR = os.homedir();
5
+ const SHELL_TOKEN_PATTERN = /"[^"]*"|'[^']*'|`[^`]*`|[^\s]+/g;
6
+ const SHELL_PATH_HINT_PATTERN = /(?:~\/?|\/|\.\/|\.\.\/|\$\{?[A-Za-z_][A-Za-z0-9_]*\}?)/;
7
+ const LEADING_ENV_PATH_PATTERN =
8
+ /^(?:\$(?<bare>[A-Za-z_][A-Za-z0-9_]*)|\$\{(?<braced>[A-Za-z_][A-Za-z0-9_]*)\})(?<suffix>(?:\/.*)?)$/;
9
+
10
+ function stripShellQuotes(value: string): string {
11
+ return value.trim().replace(/^["'`]+|["'`]+$/g, "");
12
+ }
13
+
14
+ function isLeadingEnvironmentPath(value: string): boolean {
15
+ return LEADING_ENV_PATH_PATTERN.test(value);
16
+ }
17
+
18
+ function extractPathCandidateFromToken(token: string): string | undefined {
19
+ const unquoted = stripShellQuotes(token);
20
+ if (!unquoted) {
21
+ return undefined;
22
+ }
23
+
24
+ if (isPathLikeCandidate(unquoted)) {
25
+ return unquoted;
26
+ }
27
+
28
+ const assignmentIndex = unquoted.indexOf("=");
29
+ if (assignmentIndex <= 0) {
30
+ return undefined;
31
+ }
32
+
33
+ const suffix = stripShellQuotes(unquoted.slice(assignmentIndex + 1));
34
+ return isPathLikeCandidate(suffix) ? suffix : undefined;
35
+ }
36
+
37
+ function expandLeadingEnvironmentPath(candidate: string): string {
38
+ if (candidate === "~") {
39
+ return HOME_DIR;
40
+ }
41
+ if (candidate.startsWith("~/")) {
42
+ return path.join(HOME_DIR, candidate.slice(2));
43
+ }
44
+
45
+ const match = candidate.match(LEADING_ENV_PATH_PATTERN);
46
+ if (!match) {
47
+ return candidate;
48
+ }
49
+
50
+ const variableName = match.groups?.bare ?? match.groups?.braced;
51
+ if (!variableName) {
52
+ return candidate;
53
+ }
54
+
55
+ const resolvedRoot = process.env[variableName] ?? (variableName === "HOME" ? HOME_DIR : undefined);
56
+ if (!resolvedRoot) {
57
+ return candidate;
58
+ }
59
+
60
+ const suffix = match.groups?.suffix ?? "";
61
+ if (!suffix) {
62
+ return resolvedRoot;
63
+ }
64
+
65
+ return path.join(resolvedRoot, suffix.slice(1));
66
+ }
67
+
68
+ export function isPathLikeCandidate(value: string): boolean {
69
+ const trimmed = value.trim();
70
+ return (
71
+ trimmed === "~" ||
72
+ trimmed.startsWith("/") ||
73
+ trimmed.startsWith("~/") ||
74
+ trimmed.startsWith("./") ||
75
+ trimmed.startsWith("../") ||
76
+ isLeadingEnvironmentPath(trimmed)
77
+ );
78
+ }
79
+
80
+ export function hasEmbeddedPathHint(value: string): boolean {
81
+ return SHELL_PATH_HINT_PATTERN.test(value);
82
+ }
83
+
84
+ export function extractEmbeddedPathCandidates(value: string): string[] {
85
+ const tokens = value.match(SHELL_TOKEN_PATTERN) ?? [];
86
+ const results: string[] = [];
87
+
88
+ for (const token of tokens) {
89
+ const candidate = extractPathCandidateFromToken(token);
90
+ if (candidate) {
91
+ results.push(candidate);
92
+ }
93
+ }
94
+
95
+ return results;
96
+ }
97
+
98
+ export function resolvePathCandidate(candidate: string, workspaceDir?: string): string | undefined {
99
+ if (!candidate) {
100
+ return undefined;
101
+ }
102
+
103
+ const normalized = path.normalize(expandLeadingEnvironmentPath(stripShellQuotes(candidate)));
104
+ if (path.isAbsolute(normalized)) {
105
+ return normalized;
106
+ }
107
+ if (!workspaceDir) {
108
+ return undefined;
109
+ }
110
+ return path.normalize(path.resolve(workspaceDir, normalized));
111
+ }
@@ -0,0 +1,288 @@
1
+ import type {
2
+ SensitivePathConfig,
3
+ SensitivePathMatchType,
4
+ SensitivePathRule,
5
+ SensitivePathSource,
6
+ SensitivePathStrategyOverride,
7
+ } from "../../types.ts";
8
+
9
+ const VALID_MATCH_TYPES = new Set<SensitivePathMatchType>(["prefix", "glob", "regex"]);
10
+
11
+ function builtinRule(
12
+ id: string,
13
+ assetLabel: string,
14
+ matchType: SensitivePathMatchType,
15
+ pattern: string,
16
+ ): SensitivePathRule {
17
+ return {
18
+ id,
19
+ asset_label: assetLabel,
20
+ match_type: matchType,
21
+ pattern,
22
+ source: "builtin",
23
+ };
24
+ }
25
+
26
+ const BUILTIN_SENSITIVE_PATH_RULES: SensitivePathRule[] = [
27
+ builtinRule("credential-env-files", "credential", "regex", "(?:^|/)\\.env(?:\\.[^/\\s]+)?(?:$|/)"),
28
+ builtinRule("credential-package-config-npmrc", "credential", "regex", "(?:^|/)\\.npmrc(?:$|/)"),
29
+ builtinRule("credential-package-config-pypirc", "credential", "regex", "(?:^|/)\\.pypirc(?:$|/)"),
30
+ builtinRule("credential-netrc", "credential", "regex", "(?:^|/)\\.netrc(?:$|/)"),
31
+ builtinRule("credential-ssh-directory", "credential", "regex", "(?:^|/)\\.ssh(?:/|$)"),
32
+ builtinRule("credential-gnupg-directory", "credential", "regex", "(?:^|/)\\.gnupg(?:/|$)"),
33
+ builtinRule("credential-kube-directory", "credential", "regex", "(?:^|/)(?:\\.kube|kubeconfig)(?:$|/)"),
34
+ builtinRule("credential-cloud-aws-directory", "credential", "regex", "(?:^|/)\\.aws(?:/|$)"),
35
+ builtinRule("credential-cloud-azure-directory", "credential", "regex", "(?:^|/)\\.azure(?:/|$)"),
36
+ builtinRule("credential-docker-directory", "credential", "regex", "(?:^|/)\\.docker(?:/|$)"),
37
+ builtinRule("credential-gcloud-directory", "credential", "regex", "(?:^|/)\\.config/gcloud(?:/|$)"),
38
+ builtinRule("credential-gh-directory", "credential", "regex", "(?:^|/)\\.config/gh(?:/|$)"),
39
+ builtinRule("credential-aws-file", "credential", "regex", "(?:^|/)aws/credentials(?:$|/)"),
40
+ builtinRule("credential-ssh-key-files", "credential", "regex", "(?:^|/)id_(?:rsa|ed25519|ecdsa|dsa)(?:\\.pub)?(?:$|/)"),
41
+ builtinRule("credential-known-host-files", "credential", "regex", "(?:^|/)(?:authorized_keys|known_hosts)(?:$|/)"),
42
+ builtinRule("credential-key-material-files", "credential", "regex", "(?:^|/)[^/\\s]+\\.(?:pem|p12|pfx|key)(?:$|/)"),
43
+ builtinRule("credential-client-secret-json", "credential", "regex", "(?:^|/)client_secret[^/\\s]*\\.json(?:$|/)"),
44
+
45
+ builtinRule("download-staging-downloads-directory", "download_staging", "regex", "(?:^|/)downloads(?:/|$)"),
46
+
47
+ builtinRule(
48
+ "personal-content-home-folders",
49
+ "personal_content",
50
+ "regex",
51
+ "(?:^|/)(?:users|home)/[^/]+/(?:desktop|documents|downloads|music|pictures|movies|videos|public)(?:/|$)",
52
+ ),
53
+ builtinRule(
54
+ "personal-content-mounted-home-folders",
55
+ "personal_content",
56
+ "regex",
57
+ "(?:^|/)mnt/[a-z]/users/[^/]+/(?:desktop|documents|downloads|music|pictures|videos|public)(?:/|$)",
58
+ ),
59
+ builtinRule(
60
+ "personal-content-icloud-mobile-documents",
61
+ "personal_content",
62
+ "regex",
63
+ "(?:^|/)library/mobile documents(?:/|$)",
64
+ ),
65
+ builtinRule(
66
+ "personal-content-cloudstorage",
67
+ "personal_content",
68
+ "regex",
69
+ "(?:^|/)library/cloudstorage(?:/|$)",
70
+ ),
71
+
72
+ builtinRule(
73
+ "browser-profile-macos-chromium-family",
74
+ "browser_profile",
75
+ "regex",
76
+ "(?:^|/)library/application support/(?:google/chrome|chromium|bravesoftware/brave-browser|microsoft edge|vivaldi|arc/user data)(?:/|$)",
77
+ ),
78
+ builtinRule(
79
+ "browser-profile-linux-chromium-family",
80
+ "browser_profile",
81
+ "regex",
82
+ "(?:^|/)\\.config/(?:google-chrome|chromium|bravesoftware/brave-browser|microsoft-edge|vivaldi)(?:/|$)",
83
+ ),
84
+ builtinRule("browser-profile-firefox-linux", "browser_profile", "regex", "(?:^|/)\\.mozilla/firefox(?:/|$)"),
85
+ builtinRule("browser-profile-firefox-macos", "browser_profile", "regex", "(?:^|/)library/application support/firefox(?:/|$)"),
86
+ builtinRule("browser-profile-safari", "browser_profile", "regex", "(?:^|/)library/safari(?:/|$)"),
87
+ builtinRule(
88
+ "browser-profile-safari-container",
89
+ "browser_profile",
90
+ "regex",
91
+ "(?:^|/)library/containers/com\\.apple\\.safari(?:/|$)",
92
+ ),
93
+
94
+ builtinRule(
95
+ "browser-secret-store-files",
96
+ "browser_secret_store",
97
+ "regex",
98
+ "(?:^|/)(?:cookies(?:-journal)?|login data(?: for account)?|web data|history(?:-journal)?|top sites|shortcuts|visited links|favicons|places\\.sqlite|cookies\\.sqlite|formhistory\\.sqlite|key4\\.db|key3\\.db|logins\\.json|cookies\\.binarycookies|webpageicons\\.db)(?:$|/)",
99
+ ),
100
+
101
+ builtinRule("communication-store-messages", "communication_store", "regex", "(?:^|/)library/messages(?:/|$)"),
102
+ builtinRule("communication-store-mail", "communication_store", "regex", "(?:^|/)library/mail(?:/|$)"),
103
+ builtinRule("communication-store-thunderbird", "communication_store", "regex", "(?:^|/)\\.thunderbird(?:/|$)"),
104
+ builtinRule("communication-store-linux-mail", "communication_store", "regex", "(?:^|/)\\.local/share/(?:evolution|mail)(?:/|$)"),
105
+ ];
106
+ const BUILTIN_SENSITIVE_PATH_RULE_ID_SET = new Set(BUILTIN_SENSITIVE_PATH_RULES.map((rule) => rule.id));
107
+
108
+ function trimmedString(value: unknown): string | undefined {
109
+ if (typeof value !== "string") {
110
+ return undefined;
111
+ }
112
+ const trimmed = value.trim();
113
+ return trimmed || undefined;
114
+ }
115
+
116
+ function sortRules(rules: SensitivePathRule[]): SensitivePathRule[] {
117
+ return [...rules].sort((left, right) => {
118
+ const bySource = String(left.source ?? "custom").localeCompare(String(right.source ?? "custom"));
119
+ if (bySource !== 0) {
120
+ return bySource;
121
+ }
122
+ const byLabel = left.asset_label.localeCompare(right.asset_label);
123
+ if (byLabel !== 0) {
124
+ return byLabel;
125
+ }
126
+ const byType = left.match_type.localeCompare(right.match_type);
127
+ if (byType !== 0) {
128
+ return byType;
129
+ }
130
+ return left.id.localeCompare(right.id);
131
+ });
132
+ }
133
+
134
+ function dedupeRules(rules: SensitivePathRule[]): SensitivePathRule[] {
135
+ const map = new Map<string, SensitivePathRule>();
136
+ rules.forEach((rule) => {
137
+ map.set(rule.id, rule);
138
+ });
139
+ return sortRules(Array.from(map.values()));
140
+ }
141
+
142
+ function isValidRulePattern(matchType: SensitivePathMatchType, pattern: string): boolean {
143
+ if (matchType !== "regex") {
144
+ return true;
145
+ }
146
+ try {
147
+ // Validate custom regex syntax upfront to avoid silently storing dead rules.
148
+ new RegExp(pattern, "i");
149
+ return true;
150
+ } catch {
151
+ return false;
152
+ }
153
+ }
154
+
155
+ export function cloneSensitivePathRule(rule: SensitivePathRule): SensitivePathRule {
156
+ return { ...rule };
157
+ }
158
+
159
+ export function cloneSensitivePathRules(rules: SensitivePathRule[]): SensitivePathRule[] {
160
+ return rules.map((rule) => cloneSensitivePathRule(rule));
161
+ }
162
+
163
+ export function normalizeSensitivePathRule(
164
+ value: unknown,
165
+ fallbackSource: SensitivePathSource,
166
+ ): SensitivePathRule | undefined {
167
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
168
+ return undefined;
169
+ }
170
+
171
+ const record = value as Record<string, unknown>;
172
+ const id = trimmedString(record.id);
173
+ const assetLabel = trimmedString(record.asset_label);
174
+ const matchType = trimmedString(record.match_type) as SensitivePathMatchType | undefined;
175
+ const pattern = trimmedString(record.pattern);
176
+ const source = trimmedString(record.source) as SensitivePathSource | undefined;
177
+
178
+ if (!id || !assetLabel || !matchType || !pattern || !VALID_MATCH_TYPES.has(matchType)) {
179
+ return undefined;
180
+ }
181
+ if (!isValidRulePattern(matchType, pattern)) {
182
+ return undefined;
183
+ }
184
+
185
+ return {
186
+ id,
187
+ asset_label: assetLabel,
188
+ match_type: matchType,
189
+ pattern,
190
+ source: source === "builtin" || source === "custom" ? source : fallbackSource,
191
+ };
192
+ }
193
+
194
+ export function normalizeSensitivePathRules(
195
+ value: unknown,
196
+ fallbackSource: SensitivePathSource,
197
+ ): SensitivePathRule[] {
198
+ if (!Array.isArray(value)) {
199
+ return [];
200
+ }
201
+ return dedupeRules(
202
+ value
203
+ .map((entry) => normalizeSensitivePathRule(entry, fallbackSource))
204
+ .filter((entry): entry is SensitivePathRule => Boolean(entry)),
205
+ );
206
+ }
207
+
208
+ export function getBuiltinSensitivePathRules(): SensitivePathRule[] {
209
+ return cloneSensitivePathRules(BUILTIN_SENSITIVE_PATH_RULES);
210
+ }
211
+
212
+ export function hydrateSensitivePathConfig(config?: SensitivePathConfig): SensitivePathConfig {
213
+ const builtinRules = getBuiltinSensitivePathRules();
214
+ const existingRules = normalizeSensitivePathRules(config?.path_rules, "builtin");
215
+ return {
216
+ path_rules: dedupeRules([...builtinRules, ...existingRules]),
217
+ };
218
+ }
219
+
220
+ export function normalizeSensitivePathStrategyOverride(value: unknown): SensitivePathStrategyOverride | undefined {
221
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
222
+ return undefined;
223
+ }
224
+
225
+ const record = value as Record<string, unknown>;
226
+ const disabledBuiltinIds = Array.isArray(record.disabled_builtin_ids)
227
+ ? Array.from(
228
+ new Set(
229
+ record.disabled_builtin_ids
230
+ .map((entry) => trimmedString(entry))
231
+ .filter((entry): entry is string => Boolean(entry)),
232
+ ),
233
+ )
234
+ .filter((entry) => BUILTIN_SENSITIVE_PATH_RULE_ID_SET.has(entry))
235
+ .sort((left, right) => left.localeCompare(right))
236
+ : undefined;
237
+ const customPathRules = normalizeSensitivePathRules(record.custom_path_rules, "custom")
238
+ .filter((rule) => !BUILTIN_SENSITIVE_PATH_RULE_ID_SET.has(rule.id))
239
+ .map((rule) => ({ ...rule, source: "custom" as const }));
240
+
241
+ if (!disabledBuiltinIds?.length && customPathRules.length === 0) {
242
+ return undefined;
243
+ }
244
+
245
+ return {
246
+ ...(disabledBuiltinIds?.length ? { disabled_builtin_ids: disabledBuiltinIds } : {}),
247
+ ...(customPathRules.length ? { custom_path_rules: customPathRules } : {}),
248
+ };
249
+ }
250
+
251
+ export function applySensitivePathStrategyOverride(
252
+ base: SensitivePathConfig | undefined,
253
+ override: SensitivePathStrategyOverride | undefined,
254
+ ): SensitivePathConfig {
255
+ const hydrated = hydrateSensitivePathConfig(base);
256
+ const normalizedOverride = normalizeSensitivePathStrategyOverride(override);
257
+ if (!normalizedOverride) {
258
+ return hydrated;
259
+ }
260
+
261
+ const disabledBuiltinIds = new Set(normalizedOverride.disabled_builtin_ids ?? []);
262
+ const filteredBaseRules = hydrated.path_rules.filter((rule) => {
263
+ return !(rule.source === "builtin" && disabledBuiltinIds.has(rule.id));
264
+ });
265
+
266
+ return {
267
+ path_rules: dedupeRules([
268
+ ...filteredBaseRules,
269
+ ...normalizeSensitivePathRules(normalizedOverride.custom_path_rules, "custom"),
270
+ ]),
271
+ };
272
+ }
273
+
274
+ export function listRemovedBuiltinSensitivePathRules(
275
+ base: SensitivePathConfig | undefined,
276
+ override: SensitivePathStrategyOverride | undefined,
277
+ ): SensitivePathRule[] {
278
+ const hydrated = hydrateSensitivePathConfig(base);
279
+ const normalizedOverride = normalizeSensitivePathStrategyOverride(override);
280
+ if (!normalizedOverride?.disabled_builtin_ids?.length) {
281
+ return [];
282
+ }
283
+
284
+ const disabledBuiltinIds = new Set(normalizedOverride.disabled_builtin_ids);
285
+ return hydrated.path_rules
286
+ .filter((rule) => rule.source === "builtin" && disabledBuiltinIds.has(rule.id))
287
+ .map((rule) => cloneSensitivePathRule(rule));
288
+ }
@@ -0,0 +1,161 @@
1
+ import os from "node:os";
2
+
3
+ import { getBuiltinSensitivePathRules } from "./sensitive_path_registry.ts";
4
+ import type { SensitivePathRule } from "../../types.ts";
5
+
6
+ export interface SensitivityLabelContext {
7
+ assetLabels: string[];
8
+ dataLabels: string[];
9
+ }
10
+
11
+ const OTP_PATTERN = /otp|one[- ]time|verification code|验证码|passcode|login (?:code|notification|alert)|登录提醒/i;
12
+ const HOME_DIR = os.homedir().replace(/\\/g, "/").toLowerCase();
13
+
14
+ function addLabel(labels: Set<string>, condition: boolean, label: string): void {
15
+ if (condition) {
16
+ labels.add(label);
17
+ }
18
+ }
19
+
20
+ function escapeRegExp(value: string): string {
21
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22
+ }
23
+
24
+ function globToRegExp(pattern: string): RegExp {
25
+ let source = "";
26
+ for (let index = 0; index < pattern.length; index += 1) {
27
+ const char = pattern[index];
28
+ const next = pattern[index + 1];
29
+ if (char === "*" && next === "*") {
30
+ source += ".*";
31
+ index += 1;
32
+ continue;
33
+ }
34
+ if (char === "*") {
35
+ source += "[^/]*";
36
+ continue;
37
+ }
38
+ if (char === "?") {
39
+ source += ".";
40
+ continue;
41
+ }
42
+ source += escapeRegExp(char);
43
+ }
44
+ return new RegExp(`^${source}$`, "i");
45
+ }
46
+
47
+ function normalizePathRulePattern(pattern: string): string {
48
+ const normalized = pattern.replace(/\\/g, "/").trim();
49
+ if (normalized === "~") {
50
+ return HOME_DIR;
51
+ }
52
+ if (normalized.startsWith("~/")) {
53
+ return `${HOME_DIR}/${normalized.slice(2)}`.toLowerCase();
54
+ }
55
+ if (normalized.startsWith("$HOME/")) {
56
+ return `${HOME_DIR}/${normalized.slice(6)}`.toLowerCase();
57
+ }
58
+ if (normalized.startsWith("${HOME}/")) {
59
+ return `${HOME_DIR}/${normalized.slice(8)}`.toLowerCase();
60
+ }
61
+ return normalized.toLowerCase();
62
+ }
63
+
64
+ function matchesSensitivePathRule(rule: SensitivePathRule, candidate: string): boolean {
65
+ try {
66
+ if (rule.match_type === "prefix") {
67
+ return matchesPrefixPattern(rule.pattern, candidate);
68
+ }
69
+ if (rule.match_type === "glob") {
70
+ return globToRegExp(normalizePathRulePattern(rule.pattern)).test(candidate);
71
+ }
72
+ return new RegExp(rule.pattern, "i").test(candidate);
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ function resolveSensitivePathRules(rules?: SensitivePathRule[]): SensitivePathRule[] {
79
+ if (!rules) {
80
+ return getBuiltinSensitivePathRules();
81
+ }
82
+ return rules.map((rule) => ({ ...rule }));
83
+ }
84
+
85
+ function matchesPrefixPattern(pattern: string, candidate: string): boolean {
86
+ const normalizedPrefix = normalizePathRulePattern(pattern);
87
+ if (!normalizedPrefix) {
88
+ return false;
89
+ }
90
+ if (candidate === normalizedPrefix) {
91
+ return true;
92
+ }
93
+ if (normalizedPrefix.endsWith("/")) {
94
+ return candidate.startsWith(normalizedPrefix);
95
+ }
96
+ return candidate.startsWith(`${normalizedPrefix}/`);
97
+ }
98
+
99
+ function normalizeText(value: string): string {
100
+ return value.replace(/\\/g, "/").toLowerCase();
101
+ }
102
+
103
+ function normalizePaths(resourcePaths: string[]): string[] {
104
+ return resourcePaths
105
+ .filter((value) => value.trim().length > 0)
106
+ .map((value) => normalizeText(value));
107
+ }
108
+
109
+ function combinedTextCorpus(resourcePaths: string[], toolArgsSummary: string | undefined): string {
110
+ return [...normalizePaths(resourcePaths), toolArgsSummary]
111
+ .filter((value): value is string => typeof value === "string" && value.trim().length > 0)
112
+ .map((value) => normalizeText(value))
113
+ .join(" ");
114
+ }
115
+
116
+ export function inferSensitivityLabels(
117
+ toolGroup: string | undefined,
118
+ resourcePaths: string[],
119
+ toolArgsSummary: string | undefined,
120
+ sensitivePathRules?: SensitivePathRule[],
121
+ ): SensitivityLabelContext {
122
+ const normalizedPaths = normalizePaths(resourcePaths);
123
+ const corpus = combinedTextCorpus(resourcePaths, toolArgsSummary);
124
+ const resolvedSensitivePathRules = resolveSensitivePathRules(sensitivePathRules);
125
+ const assetLabels = new Set<string>();
126
+ const dataLabels = new Set<string>();
127
+
128
+ addLabel(assetLabels, /\b(?:finance|invoice|billing|payroll|ledger)\b/.test(corpus), "financial");
129
+ addLabel(dataLabels, /\b(?:finance|invoice|billing|payroll|ledger)\b/.test(corpus), "financial");
130
+ addLabel(assetLabels, /\b(?:customer|client|crm|contact)\b/.test(corpus), "customer_data");
131
+ addLabel(dataLabels, /\b(?:customer|client|crm|contact)\b/.test(corpus), "customer_data");
132
+ addLabel(assetLabels, /\b(?:hr|personnel|resume|employee|salary)\b/.test(corpus), "hr");
133
+ addLabel(dataLabels, /\b(?:hr|personnel|resume|employee|salary)\b/.test(corpus), "pii");
134
+
135
+ const matchedSensitivePathLabels = new Set<string>();
136
+ normalizedPaths.forEach((candidate) => {
137
+ resolvedSensitivePathRules.forEach((rule) => {
138
+ if (matchesSensitivePathRule(rule, candidate)) {
139
+ matchedSensitivePathLabels.add(rule.asset_label);
140
+ }
141
+ });
142
+ });
143
+
144
+ matchedSensitivePathLabels.forEach((label) => assetLabels.add(label));
145
+ addLabel(dataLabels, matchedSensitivePathLabels.has("credential"), "secret");
146
+ addLabel(dataLabels, matchedSensitivePathLabels.has("browser_secret_store"), "browser_secret");
147
+ addLabel(dataLabels, matchedSensitivePathLabels.has("communication_store"), "communications");
148
+ addLabel(assetLabels, matchedSensitivePathLabels.has("browser_secret_store"), "browser_profile");
149
+
150
+ addLabel(dataLabels, /token|secret|password|bearer|cookie|session|jwt|private key|id_rsa/.test(corpus), "secret");
151
+ addLabel(dataLabels, OTP_PATTERN.test(corpus), "otp");
152
+ addLabel(assetLabels, /\.github\/workflows\/|dockerfile\b|terraform|\.tf\b|k8s|kubernetes|deployment\.ya?ml|secret\.ya?ml|iam/.test(corpus), "control_plane");
153
+ addLabel(dataLabels, toolGroup === "email" || toolGroup === "sms", "communications");
154
+ addLabel(dataLabels, toolGroup === "album", "media");
155
+ addLabel(dataLabels, toolGroup === "browser", "browser_secret");
156
+
157
+ return {
158
+ assetLabels: [...assetLabels],
159
+ dataLabels: [...dataLabels],
160
+ };
161
+ }