fullcourtdefense-cli 1.36.2 → 1.36.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actionPolicyEngine.d.ts +19 -0
- package/dist/actionPolicyEngine.js +157 -1
- package/dist/askHeader.d.ts +6 -0
- package/dist/askHeader.js +49 -10
- package/dist/commandNarrative.d.ts +5 -0
- package/dist/commandNarrative.js +114 -7
- package/dist/commands/daemon.js +23 -3
- package/dist/commands/demoActions.js +2 -2
- package/dist/commands/deterministicGuard.d.ts +16 -0
- package/dist/commands/deterministicGuard.js +490 -35
- package/dist/commands/doctor.js +1 -0
- package/dist/commands/login.js +4 -0
- package/dist/identityRecovery.d.ts +50 -0
- package/dist/identityRecovery.js +99 -0
- package/dist/machineIdentity.d.ts +90 -1
- package/dist/machineIdentity.js +457 -94
- package/dist/telemetry.d.ts +9 -0
- package/dist/telemetry.js +29 -7
- package/dist/version.json +1 -1
- package/package.json +2 -1
|
@@ -309,6 +309,25 @@ export declare function inferToolContext(toolName: string, args: Record<string,
|
|
|
309
309
|
operation: string;
|
|
310
310
|
context: Record<string, string>;
|
|
311
311
|
};
|
|
312
|
+
/**
|
|
313
|
+
* What kind of site a destination host is, for the few kinds that change how an
|
|
314
|
+
* upload should be read. A curated list, not a reputation feed: a host is in it
|
|
315
|
+
* only when its PURPOSE is the category (a paste site exists to publish text; a
|
|
316
|
+
* request catcher exists to capture whatever is sent to it). Deliberately absent:
|
|
317
|
+
* general cloud storage, code hosting and gists, which are everyday developer
|
|
318
|
+
* destinations and would turn the category into noise.
|
|
319
|
+
*/
|
|
320
|
+
export type DestinationCategory = 'paste' | 'file_drop' | 'request_capture' | 'tunnel' | 'messaging' | 'news';
|
|
321
|
+
export declare const DESTINATION_CATEGORY_HOSTS: Readonly<Record<DestinationCategory, readonly string[]>>;
|
|
322
|
+
/** Categories whose purpose is to receive data from strangers: a first upload there is high risk. */
|
|
323
|
+
export declare const HIGH_RISK_DESTINATION_CATEGORIES: ReadonlySet<DestinationCategory>;
|
|
324
|
+
/** The listed host itself or any subdomain of it (`www.pastebin.com`, `x.m.pipedream.net`); never a look-alike (`notpastebin.com`). */
|
|
325
|
+
export declare function destinationCategory(host: string | undefined | null): DestinationCategory | undefined;
|
|
326
|
+
/** A host's category and whether that category exists to receive data from anyone — the shape the anomaly scorer takes. */
|
|
327
|
+
export declare function classifyDestinationHost(host: string): {
|
|
328
|
+
category: DestinationCategory;
|
|
329
|
+
dataDrop: boolean;
|
|
330
|
+
} | undefined;
|
|
312
331
|
/** Exported for reproducible detector artifacts; policy decisions are not packaged. */
|
|
313
332
|
export declare function inferBundledToolContext(toolName: string, args: Record<string, any>): {
|
|
314
333
|
operation: string;
|
|
@@ -24,14 +24,18 @@ __export(actionPolicyEngine_exports, {
|
|
|
24
24
|
ACTION_POLICY_GRANT_MAX_USES: () => ACTION_POLICY_GRANT_MAX_USES,
|
|
25
25
|
CONTENT_FACT_FIELDS: () => CONTENT_FACT_FIELDS,
|
|
26
26
|
DEFAULT_ACTION_POLICY_APPROVAL: () => DEFAULT_ACTION_POLICY_APPROVAL,
|
|
27
|
+
DESTINATION_CATEGORY_HOSTS: () => DESTINATION_CATEGORY_HOSTS,
|
|
27
28
|
GCLOUD_DELETE_VERB_RE: () => GCLOUD_DELETE_VERB_RE,
|
|
28
29
|
GCLOUD_READ_VERB_RE: () => GCLOUD_READ_VERB_RE,
|
|
29
30
|
GCLOUD_WRITE_VERB_RE: () => GCLOUD_WRITE_VERB_RE,
|
|
31
|
+
HIGH_RISK_DESTINATION_CATEGORIES: () => HIGH_RISK_DESTINATION_CATEGORIES,
|
|
30
32
|
MACHINE_KINDS: () => MACHINE_KINDS,
|
|
31
33
|
WORKSTATION_ASK_APPROVAL: () => WORKSTATION_ASK_APPROVAL,
|
|
32
34
|
approvalExceptionError: () => approvalExceptionError,
|
|
33
35
|
canonicalAgentClient: () => canonicalAgentClient,
|
|
36
|
+
classifyDestinationHost: () => classifyDestinationHost,
|
|
34
37
|
deriveToolOperations: () => deriveToolOperations,
|
|
38
|
+
destinationCategory: () => destinationCategory,
|
|
35
39
|
detectSecretKind: () => detectSecretKind,
|
|
36
40
|
effectiveApprovalScope: () => effectiveApprovalScope,
|
|
37
41
|
evaluateActionPolicies: () => evaluateActionPolicies,
|
|
@@ -6888,6 +6892,7 @@ var OPERATION_MATCH_SKIP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
6888
6892
|
"destination.host",
|
|
6889
6893
|
"destination.type",
|
|
6890
6894
|
"destination.repository",
|
|
6895
|
+
"destination.category",
|
|
6891
6896
|
"destinationHost",
|
|
6892
6897
|
"host",
|
|
6893
6898
|
"hostname",
|
|
@@ -7461,8 +7466,17 @@ var GIT_NETWORK_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
|
7461
7466
|
"receive-pack",
|
|
7462
7467
|
"lfs"
|
|
7463
7468
|
]);
|
|
7469
|
+
var POWERSHELL_LINE_RE = /\$env:|-ErrorAction\b|\bWrite-(?:Output|Host)\b|\b(?:Get|Set|New|Remove|Start|Stop|Invoke|Select|Where|ForEach|Test|Add|Format|Out|ConvertTo|ConvertFrom|Import|Export|Measure|Sort|Group|Restart|Register|Unregister|Enable|Disable|Copy|Move|Rename|Resolve|Split|Join|Read|Wait|Clear)-[A-Z][A-Za-z]+\b|@\{|@'|@"|\[(?:System\.|Security\.|IO\.|math\])|\$_\.|\$\w+\s*=\s*(?:Get|New|Read)-|\|\s*%\s|\|\s*\?\s/;
|
|
7470
|
+
var POWERSHELL_ONLY_SYNTAX_RE = /(?:^|[\s;{(|])\$[A-Za-z_][\w:]*\s*[+-]?=(?!=)|`"|(?:^|[\s;{])(?:if|elseif|foreach|while|switch)\s*\((?!\()|\}\s*(?:else|elseif|catch|finally)\b|\|\s*(?:where|foreach|%|\?)\s*\{/i;
|
|
7471
|
+
var POWERSHELL_ESCAPES_ONLY_RE = /^(?:[^`]|`[$"'`0abefnrtuv])*$/;
|
|
7472
|
+
function backtickMayExecute(segment) {
|
|
7473
|
+
if (!segment.includes("`")) return false;
|
|
7474
|
+
const code = maskQuotedSpans(segment);
|
|
7475
|
+
const powershell = POWERSHELL_LINE_RE.test(code) || POWERSHELL_ONLY_SYNTAX_RE.test(code);
|
|
7476
|
+
return !(powershell && POWERSHELL_ESCAPES_ONLY_RE.test(segment));
|
|
7477
|
+
}
|
|
7464
7478
|
function segmentMayReachNetwork(segment) {
|
|
7465
|
-
if (
|
|
7479
|
+
if (segment.includes("$(") || backtickMayExecute(segment) || PS_VALUE_EXECUTOR_RE.test(segment)) return true;
|
|
7466
7480
|
return splitShellSegments(segment, true).some((stage) => {
|
|
7467
7481
|
if (/^\s*(?:\$[\w:]+|[A-Za-z_]\w*)\s*=/.test(stage)) return true;
|
|
7468
7482
|
const masked = maskQuotedSpans(stage);
|
|
@@ -8758,8 +8772,145 @@ function inferToolContext(toolName, args) {
|
|
|
8758
8772
|
}
|
|
8759
8773
|
applyRemoteControlFact(toolName, args, context);
|
|
8760
8774
|
applyWebReadFacts(toolName, args, context);
|
|
8775
|
+
applyDestinationCategoryFact(context);
|
|
8761
8776
|
return { operation, context };
|
|
8762
8777
|
}
|
|
8778
|
+
var DESTINATION_CATEGORY_HOSTS = {
|
|
8779
|
+
paste: [
|
|
8780
|
+
"pastebin.com",
|
|
8781
|
+
"paste.ee",
|
|
8782
|
+
"hastebin.com",
|
|
8783
|
+
"rentry.co",
|
|
8784
|
+
"rentry.org",
|
|
8785
|
+
"dpaste.org",
|
|
8786
|
+
"dpaste.com",
|
|
8787
|
+
"paste.rs",
|
|
8788
|
+
"termbin.com",
|
|
8789
|
+
"controlc.com",
|
|
8790
|
+
"justpaste.it",
|
|
8791
|
+
"privatebin.net",
|
|
8792
|
+
"ix.io",
|
|
8793
|
+
"sprunge.us",
|
|
8794
|
+
"pastes.io",
|
|
8795
|
+
"paste.mozilla.org"
|
|
8796
|
+
],
|
|
8797
|
+
file_drop: [
|
|
8798
|
+
"transfer.sh",
|
|
8799
|
+
"file.io",
|
|
8800
|
+
"0x0.st",
|
|
8801
|
+
"temp.sh",
|
|
8802
|
+
"bashupload.com",
|
|
8803
|
+
"gofile.io",
|
|
8804
|
+
"catbox.moe",
|
|
8805
|
+
"uguu.se",
|
|
8806
|
+
"tmpfiles.org",
|
|
8807
|
+
"filebin.net",
|
|
8808
|
+
"wetransfer.com",
|
|
8809
|
+
"we.tl",
|
|
8810
|
+
"sendspace.com",
|
|
8811
|
+
"mediafire.com",
|
|
8812
|
+
"mega.nz",
|
|
8813
|
+
"anonfiles.com",
|
|
8814
|
+
"oshi.at",
|
|
8815
|
+
"pixeldrain.com",
|
|
8816
|
+
"ufile.io",
|
|
8817
|
+
"keep.sh",
|
|
8818
|
+
"filetransfer.io"
|
|
8819
|
+
],
|
|
8820
|
+
request_capture: [
|
|
8821
|
+
"webhook.site",
|
|
8822
|
+
"pipedream.net",
|
|
8823
|
+
"beeceptor.com",
|
|
8824
|
+
"hookbin.com",
|
|
8825
|
+
"requestcatcher.com",
|
|
8826
|
+
"interact.sh",
|
|
8827
|
+
"oast.fun",
|
|
8828
|
+
"oast.pro",
|
|
8829
|
+
"oast.live",
|
|
8830
|
+
"oast.site",
|
|
8831
|
+
"oast.online",
|
|
8832
|
+
"oast.me",
|
|
8833
|
+
"oastify.com",
|
|
8834
|
+
"burpcollaborator.net",
|
|
8835
|
+
"dnslog.cn",
|
|
8836
|
+
"ceye.io"
|
|
8837
|
+
],
|
|
8838
|
+
tunnel: [
|
|
8839
|
+
"ngrok.io",
|
|
8840
|
+
"ngrok.app",
|
|
8841
|
+
"ngrok.dev",
|
|
8842
|
+
"ngrok-free.app",
|
|
8843
|
+
"ngrok-free.dev",
|
|
8844
|
+
"trycloudflare.com",
|
|
8845
|
+
"loca.lt",
|
|
8846
|
+
"localtunnel.me",
|
|
8847
|
+
"serveo.net",
|
|
8848
|
+
"localhost.run",
|
|
8849
|
+
"lhr.life",
|
|
8850
|
+
"pinggy.io",
|
|
8851
|
+
"pinggy.link",
|
|
8852
|
+
"bore.pub"
|
|
8853
|
+
],
|
|
8854
|
+
messaging: ["discord.com", "discordapp.com", "api.telegram.org", "hooks.slack.com"],
|
|
8855
|
+
news: [
|
|
8856
|
+
"ynet.co.il",
|
|
8857
|
+
"ynetnews.com",
|
|
8858
|
+
"walla.co.il",
|
|
8859
|
+
"mako.co.il",
|
|
8860
|
+
"n12.co.il",
|
|
8861
|
+
"haaretz.co.il",
|
|
8862
|
+
"haaretz.com",
|
|
8863
|
+
"themarker.com",
|
|
8864
|
+
"maariv.co.il",
|
|
8865
|
+
"globes.co.il",
|
|
8866
|
+
"calcalist.co.il",
|
|
8867
|
+
"israelhayom.co.il",
|
|
8868
|
+
"kan.org.il",
|
|
8869
|
+
"timesofisrael.com",
|
|
8870
|
+
"jpost.com",
|
|
8871
|
+
"i24news.tv",
|
|
8872
|
+
"bbc.com",
|
|
8873
|
+
"bbc.co.uk",
|
|
8874
|
+
"cnn.com",
|
|
8875
|
+
"nytimes.com",
|
|
8876
|
+
"reuters.com",
|
|
8877
|
+
"apnews.com",
|
|
8878
|
+
"theguardian.com",
|
|
8879
|
+
"washingtonpost.com",
|
|
8880
|
+
"wsj.com",
|
|
8881
|
+
"bloomberg.com",
|
|
8882
|
+
"ft.com",
|
|
8883
|
+
"foxnews.com",
|
|
8884
|
+
"nbcnews.com",
|
|
8885
|
+
"cbsnews.com",
|
|
8886
|
+
"npr.org",
|
|
8887
|
+
"aljazeera.com"
|
|
8888
|
+
]
|
|
8889
|
+
};
|
|
8890
|
+
var HIGH_RISK_DESTINATION_CATEGORIES = /* @__PURE__ */ new Set(["paste", "file_drop", "request_capture", "tunnel"]);
|
|
8891
|
+
var DESTINATION_CATEGORY_BY_HOST = new Map(
|
|
8892
|
+
Object.entries(DESTINATION_CATEGORY_HOSTS).flatMap(([category, hosts]) => hosts.map((host) => [host, category]))
|
|
8893
|
+
);
|
|
8894
|
+
function destinationCategory(host) {
|
|
8895
|
+
let candidate = String(host || "").trim().toLowerCase().replace(/\.$/, "");
|
|
8896
|
+
while (candidate) {
|
|
8897
|
+
const category = DESTINATION_CATEGORY_BY_HOST.get(candidate);
|
|
8898
|
+
if (category) return category;
|
|
8899
|
+
const dot = candidate.indexOf(".");
|
|
8900
|
+
if (dot < 0) return void 0;
|
|
8901
|
+
candidate = candidate.slice(dot + 1);
|
|
8902
|
+
}
|
|
8903
|
+
return void 0;
|
|
8904
|
+
}
|
|
8905
|
+
function classifyDestinationHost(host) {
|
|
8906
|
+
const category = destinationCategory(host);
|
|
8907
|
+
return category ? { category, dataDrop: HIGH_RISK_DESTINATION_CATEGORIES.has(category) } : void 0;
|
|
8908
|
+
}
|
|
8909
|
+
function applyDestinationCategoryFact(context) {
|
|
8910
|
+
delete context["destination.category"];
|
|
8911
|
+
const category = destinationCategory(context["destination.domain"]);
|
|
8912
|
+
if (category) context["destination.category"] = category;
|
|
8913
|
+
}
|
|
8763
8914
|
function applyRemoteControlFact(toolName, args, context) {
|
|
8764
8915
|
delete context["toolArgs.remoteControl"];
|
|
8765
8916
|
const shellText = remoteControlProvider ? shellCommandTextOf(toolName, args) : void 0;
|
|
@@ -8911,6 +9062,7 @@ function inferBundledToolContext(toolName, args) {
|
|
|
8911
9062
|
}
|
|
8912
9063
|
applyRemoteControlFact(toolName, args, context);
|
|
8913
9064
|
applyWebReadFacts(toolName, args, context);
|
|
9065
|
+
applyDestinationCategoryFact(context);
|
|
8914
9066
|
return { operation, context };
|
|
8915
9067
|
}
|
|
8916
9068
|
var RESERVED_INPUT_FACTS = /* @__PURE__ */ new Set([
|
|
@@ -9012,14 +9164,18 @@ function inventoryToolMatchesActionPolicy(toolName, toolActions, policies) {
|
|
|
9012
9164
|
ACTION_POLICY_GRANT_MAX_USES,
|
|
9013
9165
|
CONTENT_FACT_FIELDS,
|
|
9014
9166
|
DEFAULT_ACTION_POLICY_APPROVAL,
|
|
9167
|
+
DESTINATION_CATEGORY_HOSTS,
|
|
9015
9168
|
GCLOUD_DELETE_VERB_RE,
|
|
9016
9169
|
GCLOUD_READ_VERB_RE,
|
|
9017
9170
|
GCLOUD_WRITE_VERB_RE,
|
|
9171
|
+
HIGH_RISK_DESTINATION_CATEGORIES,
|
|
9018
9172
|
MACHINE_KINDS,
|
|
9019
9173
|
WORKSTATION_ASK_APPROVAL,
|
|
9020
9174
|
approvalExceptionError,
|
|
9021
9175
|
canonicalAgentClient,
|
|
9176
|
+
classifyDestinationHost,
|
|
9022
9177
|
deriveToolOperations,
|
|
9178
|
+
destinationCategory,
|
|
9023
9179
|
detectSecretKind,
|
|
9024
9180
|
effectiveApprovalScope,
|
|
9025
9181
|
evaluateActionPolicies,
|
package/dist/askHeader.d.ts
CHANGED
|
@@ -67,6 +67,12 @@ export interface AskHeader {
|
|
|
67
67
|
}
|
|
68
68
|
/** The client id as the console labels it; undefined for a missing or placeholder id. */
|
|
69
69
|
export declare function agentClientLabel(raw?: string): string | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* The guard's credential-command patterns (`deterministicGuard` CREDENTIAL_COMMAND_RULES), for
|
|
72
|
+
* naming a secret the line sends somewhere when ANOTHER rule asked. Copied because this module stays
|
|
73
|
+
* pure (the guard reads files); test-ask-dialog.js fails when the two differ.
|
|
74
|
+
*/
|
|
75
|
+
export declare const LINE_CREDENTIAL_PATTERNS: Readonly<Record<string, string>>;
|
|
70
76
|
/** Name tokens of a project id → environment. Deterministic, and only a hint: the dialog says "(by name)". */
|
|
71
77
|
export declare function environmentTag(projectId: string): EnvironmentTag | undefined;
|
|
72
78
|
interface ProjectRead {
|
package/dist/askHeader.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LINE_CREDENTIAL_PATTERNS = void 0;
|
|
3
4
|
exports.agentClientLabel = agentClientLabel;
|
|
4
5
|
exports.environmentTag = environmentTag;
|
|
5
6
|
exports.projectsInCommand = projectsInCommand;
|
|
@@ -17,6 +18,7 @@ exports.buildAskHeader = buildAskHeader;
|
|
|
17
18
|
* browser extension bundles `approvalPrompt.ts` (and so this module) for the browser.
|
|
18
19
|
*/
|
|
19
20
|
const commandNarrative_1 = require("./commandNarrative");
|
|
21
|
+
const shellQuoteMask_1 = require("./shellQuoteMask");
|
|
20
22
|
const MAX_COMMAND = 90;
|
|
21
23
|
/** The console's client names (`frontend/src/lib/activityClient.ts`): the dialog and the console name the same agent. */
|
|
22
24
|
const CLIENT_LABELS = {
|
|
@@ -186,6 +188,24 @@ const TOKEN_PRINTER_ACTIONS = [
|
|
|
186
188
|
{ re: /\bgcloud(?:\.cmd|\.exe)?\s+(?:[\w-]+\s+)*?auth\s+print-identity-token\b/i, action: 'Reads a cloud identity token' },
|
|
187
189
|
{ re: /\bgh(?:\.exe)?\s+auth\s+token\b/i, action: 'Reads the GitHub token' },
|
|
188
190
|
];
|
|
191
|
+
/**
|
|
192
|
+
* The guard's credential-command patterns (`deterministicGuard` CREDENTIAL_COMMAND_RULES), for
|
|
193
|
+
* naming a secret the line sends somewhere when ANOTHER rule asked. Copied because this module stays
|
|
194
|
+
* pure (the guard reads files); test-ask-dialog.js fails when the two differ.
|
|
195
|
+
*/
|
|
196
|
+
exports.LINE_CREDENTIAL_PATTERNS = {
|
|
197
|
+
gh_auth_token_cmd: String.raw `\bgh\s+auth\s+token\b|\bgh\s+auth\s+status\b(?=[^\n;|&]*(?:--show-token|\s-t\b))`,
|
|
198
|
+
git_credential_fill: String.raw `\bgit\s+credential(?:-manager|-store|-cache)?\s+(?:fill|get)\b`,
|
|
199
|
+
macos_keychain_dump: String.raw `\bsecurity\s+(?:find-generic-password|find-internet-password)\b[^\n;|&]*\s-w\b|\bsecurity\s+dump-keychain\b`,
|
|
200
|
+
kubectl_config_raw: String.raw `\bkubectl\s+config\s+view\b[^\n;|&]*--raw\b`,
|
|
201
|
+
aws_export_credentials: String.raw `\baws\s+configure\s+export-credentials\b|\baws\s+configure\s+get\b[^\n;|&]*secret`,
|
|
202
|
+
gcloud_access_token_cmd: String.raw `\bgcloud\s+auth\b[^\n;|&]*\bprint-access-token\b`,
|
|
203
|
+
gcloud_secret_access_cmd: String.raw `\bgcloud\s+secrets\s+versions\s+access\b`,
|
|
204
|
+
az_access_token_cmd: String.raw `\baz\s+account\s+get-access-token\b`,
|
|
205
|
+
aws_secret_fetch_cmd: String.raw `\baws\s+secretsmanager\s+get-secret-value\b|\baws\s+ssm\s+get-parameter\b[^\n;|&]*--with-decryption\b`,
|
|
206
|
+
vault_read_cmd: String.raw `\bvault\s+(?:kv\s+get|read)\b`,
|
|
207
|
+
};
|
|
208
|
+
const LINE_CREDENTIAL = Object.entries(exports.LINE_CREDENTIAL_PATTERNS).map(([item, pattern]) => ({ item, re: new RegExp(pattern, 'i') }));
|
|
189
209
|
const HOW_RANK = ['delete', 'permissions', 'protected', 'change', 'read'];
|
|
190
210
|
/** Name tokens of a project id → environment. Deterministic, and only a hint: the dialog says "(by name)". */
|
|
191
211
|
function environmentTag(projectId) {
|
|
@@ -272,22 +292,31 @@ function oneLine(text, max = MAX_COMMAND) {
|
|
|
272
292
|
tail = tail.slice(tailSpace + 1);
|
|
273
293
|
return `${head.trimEnd()} … ${tail.trimStart()}`;
|
|
274
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* The line as the shell reads it (`maskShell`, dialect from the line): quoted
|
|
297
|
+
* text keeps its length but loses spaces, quotes and `$`, so no flag, URL or
|
|
298
|
+
* program starts inside a commit message or issue body. Facts come from here.
|
|
299
|
+
*/
|
|
300
|
+
function codeOf(command) {
|
|
301
|
+
return (0, shellQuoteMask_1.maskShell)(command, (0, shellQuoteMask_1.shellDialectOf)(command)).code;
|
|
302
|
+
}
|
|
275
303
|
/** Literal project ids named on the command: `--project x` / `--project=x` and Google API `projects/<id>` paths. */
|
|
276
304
|
function projectsInCommand(command) {
|
|
305
|
+
const code = codeOf(command);
|
|
277
306
|
const ids = [];
|
|
278
307
|
let computed = false;
|
|
279
308
|
const add = (raw) => {
|
|
280
309
|
const value = raw.replace(/^["']|["']$/g, '');
|
|
281
|
-
if (/[$`(){}%<>]/.test(value) || !PROJECT_ID_RE.test(value.toLowerCase())) {
|
|
310
|
+
if (/[$`(){}%<>_]/.test(value) || !PROJECT_ID_RE.test(value.toLowerCase())) {
|
|
282
311
|
computed = true;
|
|
283
312
|
return;
|
|
284
313
|
}
|
|
285
314
|
if (!ids.includes(value.toLowerCase()))
|
|
286
315
|
ids.push(value.toLowerCase());
|
|
287
316
|
};
|
|
288
|
-
for (const m of
|
|
317
|
+
for (const m of code.matchAll(/(?:^|\s)--project(?:=|\s+)("[^"]*"|'[^']*'|[^\s;|&]+)/g))
|
|
289
318
|
add(m[1]);
|
|
290
|
-
for (const m of
|
|
319
|
+
for (const m of code.matchAll(/(?:^|[\s=(,"'])https?:\/\/[a-z0-9.-]*googleapis\.com\/[^\s"'`]*?\bprojects\/([^/\s"'`?#]+)/gi))
|
|
291
320
|
add(m[1]);
|
|
292
321
|
return { ids, computed };
|
|
293
322
|
}
|
|
@@ -441,15 +470,22 @@ function buildAskHeader(input) {
|
|
|
441
470
|
const command = (input.command || '').trim();
|
|
442
471
|
const shell = subject.event ? subject.event === 'shell' : /command$/.test(input.kind || '') || (!input.tool && Boolean(command));
|
|
443
472
|
const noun = nounOf(input.kind, subject.event);
|
|
444
|
-
const
|
|
473
|
+
const askedItem = ruleItem(subject, input.ruleDetail, input.policy);
|
|
474
|
+
const code = shell ? codeOf(command) : command;
|
|
475
|
+
const printer = shell ? TOKEN_PRINTER_ACTIONS.find(p => p.re.test(code)) : undefined;
|
|
476
|
+
// Another rule asked (egress, a cloud policy), but the line reads a credential and sends it to a host:
|
|
477
|
+
// that is the fact the developer must see (the credential rule itself may only warn, #1837).
|
|
478
|
+
const lineItem = shell && !(askedItem && CREDENTIAL_ACTIONS[askedItem]) ? LINE_CREDENTIAL.find(c => c.re.test(code))?.item : undefined;
|
|
479
|
+
const flows = shell && (lineItem || printer) ? (0, commandNarrative_1.capturedValueDestinations)(command, s => LINE_CREDENTIAL.some(c => c.re.test(codeOf(s)))) : [];
|
|
480
|
+
const viaFlow = Boolean(lineItem && flows.length);
|
|
481
|
+
const item = viaFlow ? lineItem : askedItem;
|
|
445
482
|
const credentialAction = item ? CREDENTIAL_ACTIONS[item] : undefined;
|
|
446
483
|
const cloud = cloudEntries(subject.cloud);
|
|
447
484
|
const how = cloud.length ? strongestHow(cloud) : undefined;
|
|
448
485
|
const decisiveEntries = how ? cloud.filter(entry => entry.how === how) : [];
|
|
449
486
|
const decisiveTools = unique(decisiveEntries.map(entry => entry.tool));
|
|
450
|
-
const printer = shell ? TOKEN_PRINTER_ACTIONS.find(p => p.re.test(command)) : undefined;
|
|
451
487
|
// A token read is about the token, even when the line also reaches a target not read yet (`curl` POST).
|
|
452
|
-
const credential = Boolean(credentialAction || (printer && (!how || how === '?')));
|
|
488
|
+
const credential = Boolean(credentialAction || (printer && (!how || how === '?' || flows.length > 0)));
|
|
453
489
|
// Where: a credential line names the project its literal Google API URL / --project acts on;
|
|
454
490
|
// otherwise what the cloud entries name (each unknown part stays "unknown"), else a literal
|
|
455
491
|
// project on the command, else the host the action reaches.
|
|
@@ -458,7 +494,7 @@ function buildAskHeader(input) {
|
|
|
458
494
|
const cloudWheres = unique(cloud.map(entryWhere));
|
|
459
495
|
const names = useCloud ? unique(cloud.flatMap(environmentNames)) : literal.ids;
|
|
460
496
|
const host = subject.destinationDomain || (subject.credentialHosts || [])[0]
|
|
461
|
-
|| (shell ? /https?:\/\/([a-z0-9.-]+\.[a-z]{2,})(?=[/:\s"'`]|$)/i.exec(
|
|
497
|
+
|| (shell ? /(?:^|[\s=(,"'])https?:\/\/([a-z0-9.-]+\.[a-z]{2,})(?=[/:\s"'`]|$)/i.exec(code)?.[1]?.toLowerCase() : undefined);
|
|
462
498
|
const tag = names.length ? strongestTag(names) : undefined;
|
|
463
499
|
const hint = addHint(input.alwaysNotOffered, unique(cloud.map(entry => entry.tool)));
|
|
464
500
|
let where;
|
|
@@ -481,7 +517,10 @@ function buildAskHeader(input) {
|
|
|
481
517
|
if (credential) {
|
|
482
518
|
source = 'credential';
|
|
483
519
|
action = credentialAction || printer.action;
|
|
484
|
-
|
|
520
|
+
const sentTo = (subject.credentialHosts && subject.credentialHosts.length ? subject.credentialHosts : flows).slice(0, 3);
|
|
521
|
+
why = viaFlow && sentTo.length
|
|
522
|
+
? `This ${noun} ${action.charAt(0).toLowerCase()}${action.slice(1)} and sends it to ${sentTo.join(', ')}.`
|
|
523
|
+
: `This ${noun} ${action.charAt(0).toLowerCase()}${action.slice(1)}.`;
|
|
485
524
|
}
|
|
486
525
|
else if (how) {
|
|
487
526
|
source = 'cloud';
|
|
@@ -533,7 +572,7 @@ function buildAskHeader(input) {
|
|
|
533
572
|
why = input.policy ? `Rule "${oneLine(input.policy, 80)}" asks before this ${noun} runs.` : `A policy asks before this ${noun} runs.`;
|
|
534
573
|
}
|
|
535
574
|
const decisive = !shell ? undefined
|
|
536
|
-
: source === 'credential' ? matchedSegment(command, s => TOKEN_PRINTER_ACTIONS.some(p => p.re.test(s)) || (item ? new RegExp(`\\b${(CREDENTIAL_PROGRAM[item] || '\\S')}`, 'i').test(s) : false))
|
|
575
|
+
: source === 'credential' ? matchedSegment(command, s => TOKEN_PRINTER_ACTIONS.some(p => p.re.test(codeOf(s))) || (item ? new RegExp(`\\b${(CREDENTIAL_PROGRAM[item] || '\\S')}`, 'i').test(codeOf(s)) : false))
|
|
537
576
|
: source === 'cloud' ? (matchedSegment(command, s => toolSegment(decisiveTools, how).test(s) && !/\bgcloud\s+(?:auth|config)\b/i.test(s))
|
|
538
577
|
|| matchedSegment(command, s => toolSegment(decisiveTools, 'any').test(s)))
|
|
539
578
|
: undefined;
|
|
@@ -545,7 +584,7 @@ function buildAskHeader(input) {
|
|
|
545
584
|
let always = alwaysRow(input);
|
|
546
585
|
if (credential) {
|
|
547
586
|
const bound = subject.credentialHosts || [];
|
|
548
|
-
const followed = bound.length ? [] : (0, commandNarrative_1.tokenDestinations)(command);
|
|
587
|
+
const followed = bound.length ? [] : flows.length ? flows : (0, commandNarrative_1.tokenDestinations)(command);
|
|
549
588
|
const label = (item && TOKEN_RULES.has(item)) || (!credentialAction && printer) ? 'Token sent to' : 'Secret sent to';
|
|
550
589
|
secretSentTo = { label, value: [...bound, ...followed].slice(0, 3).join(', ') || 'unknown' };
|
|
551
590
|
// A destination IS readable, but another step on the line is not: say that, not "could not be read".
|
|
@@ -39,6 +39,11 @@ export declare function splitCommandSteps(command: string): string[];
|
|
|
39
39
|
export declare function shortCode(value: string, max?: number): string;
|
|
40
40
|
/** Every host a token read on this line is sent to, as the step list names it. Empty when none is readable. */
|
|
41
41
|
export declare function tokenDestinations(command: string): string[];
|
|
42
|
+
/**
|
|
43
|
+
* Every host a value stored by an assignment is sent to, when the assignment's value is one `reads`
|
|
44
|
+
* recognizes (a secret read the narrative does not capture), plus what `tokenDestinations` follows.
|
|
45
|
+
*/
|
|
46
|
+
export declare function capturedValueDestinations(command: string, reads: (segment: string) => boolean): string[];
|
|
42
47
|
/** One plain-words line per step, in order. */
|
|
43
48
|
export declare function describeShellCommand(command: string): CommandStep[];
|
|
44
49
|
/** The step lines as the explanation prints them (`- …`). */
|
package/dist/commandNarrative.js
CHANGED
|
@@ -18,8 +18,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
18
18
|
exports.splitCommandSteps = splitCommandSteps;
|
|
19
19
|
exports.shortCode = shortCode;
|
|
20
20
|
exports.tokenDestinations = tokenDestinations;
|
|
21
|
+
exports.capturedValueDestinations = capturedValueDestinations;
|
|
21
22
|
exports.describeShellCommand = describeShellCommand;
|
|
22
23
|
exports.describeShellCommandLines = describeShellCommandLines;
|
|
24
|
+
const deterministicGuard_1 = require("./commands/deterministicGuard");
|
|
23
25
|
const MAX_STEPS = 14;
|
|
24
26
|
const MAX_LINE = 220;
|
|
25
27
|
/** Token printers whose output can be assigned; `issuer` = who accepts the token (identity tokens go to their audience). */
|
|
@@ -642,7 +644,7 @@ function describeOne(segmentIn, captured, fns = new Map()) {
|
|
|
642
644
|
case 'python3':
|
|
643
645
|
case 'tsx':
|
|
644
646
|
case 'ts-node':
|
|
645
|
-
return step(`Runs ${program} ${rest[1] ? `(${shortValue(rest.slice(1).join(' '), 60)})` : ''}`, false);
|
|
647
|
+
return step(`Runs ${program} ${rest[1] ? `(${/[\\/]/.test(rest[1]) && !rest[1].startsWith('-') ? [shortPath(rest[1]), ...rest.slice(2)].join(' ').slice(0, 80) : shortValue(rest.slice(1).join(' '), 60)})` : ''}`, false);
|
|
646
648
|
case 'curl':
|
|
647
649
|
case 'wget':
|
|
648
650
|
case 'invoke-webrequest':
|
|
@@ -677,7 +679,10 @@ function describeOne(segmentIn, captured, fns = new Map()) {
|
|
|
677
679
|
case 'mkdir':
|
|
678
680
|
case 'new-item':
|
|
679
681
|
case 'ni':
|
|
680
|
-
case 'touch':
|
|
682
|
+
case 'touch': {
|
|
683
|
+
const made = rest.slice(1).filter(t => !t.startsWith('-'));
|
|
684
|
+
return step(`Creates ${made.length === 1 ? shortPath(made[0]) : shortValue(made.join(', '), 60)}`);
|
|
685
|
+
}
|
|
681
686
|
case 'cat':
|
|
682
687
|
case 'get-content':
|
|
683
688
|
case 'gc':
|
|
@@ -685,8 +690,22 @@ function describeOne(segmentIn, captured, fns = new Map()) {
|
|
|
685
690
|
case 'head':
|
|
686
691
|
case 'tail':
|
|
687
692
|
case 'less':
|
|
688
|
-
case 'more':
|
|
689
|
-
|
|
693
|
+
case 'more': {
|
|
694
|
+
// `cat > f` / `cat 2>/dev/null f`: redirections and their targets are not files read.
|
|
695
|
+
const operands = [];
|
|
696
|
+
for (let k = 1; k < rest.length; k++) {
|
|
697
|
+
if (/^(?:\d*>{1,2}|&>|<)$/.test(rest[k])) {
|
|
698
|
+
k++;
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
if (/^(?:\d*>|&>|<)/.test(rest[k]) || rest[k].startsWith('-'))
|
|
702
|
+
continue;
|
|
703
|
+
operands.push(rest[k]);
|
|
704
|
+
}
|
|
705
|
+
if (!operands.length && redirectTo)
|
|
706
|
+
return step(`Writes its input to ${shortValue(redirectTo, 60)}`);
|
|
707
|
+
return step(`Reads the file ${shortValue(operands[0] || '', 60)}${redirectNote}`);
|
|
708
|
+
}
|
|
690
709
|
case 'ls':
|
|
691
710
|
case 'dir':
|
|
692
711
|
case 'get-childitem':
|
|
@@ -904,6 +923,32 @@ function tokenDestinations(command) {
|
|
|
904
923
|
} });
|
|
905
924
|
return [...new Set(described.flatMap((step, index) => step.captures ? tokenHosts(described, index, step.captures.variable) : []))].sort();
|
|
906
925
|
}
|
|
926
|
+
/** `$v = …` (PowerShell) or `v=$(…)` / `export v=$(…)` (POSIX): the variable an assignment step stores. */
|
|
927
|
+
function assignedVariable(segment) {
|
|
928
|
+
return /^\s*\$([\w:]+)\s*=(?!=)/.exec(segment)?.[1] || /^\s*(?:export\s+)?([A-Za-z_]\w*)=\$\(/.exec(segment)?.[1];
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Every host a value stored by an assignment is sent to, when the assignment's value is one `reads`
|
|
932
|
+
* recognizes (a secret read the narrative does not capture), plus what `tokenDestinations` follows.
|
|
933
|
+
*/
|
|
934
|
+
function capturedValueDestinations(command, reads) {
|
|
935
|
+
const steps = splitCommandSteps(command);
|
|
936
|
+
const fns = localFunctions(command);
|
|
937
|
+
const described = steps.map(segment => { try {
|
|
938
|
+
return describeOne(segment, undefined, fns);
|
|
939
|
+
}
|
|
940
|
+
catch {
|
|
941
|
+
return { segment, text: '', known: false };
|
|
942
|
+
} });
|
|
943
|
+
const hosts = new Set();
|
|
944
|
+
described.forEach((step, index) => {
|
|
945
|
+
const variable = step.captures?.variable || (reads(step.segment) ? assignedVariable(step.segment) : undefined);
|
|
946
|
+
if (variable)
|
|
947
|
+
for (const host of tokenHosts(described, index, variable))
|
|
948
|
+
hosts.add(host);
|
|
949
|
+
});
|
|
950
|
+
return [...hosts].sort();
|
|
951
|
+
}
|
|
907
952
|
function noteTokenDestinations(steps) {
|
|
908
953
|
return steps.map((step, index) => {
|
|
909
954
|
if (!step.captures)
|
|
@@ -921,13 +966,75 @@ function noteTokenDestinations(steps) {
|
|
|
921
966
|
return { ...step, text: shortValue(`${step.text}, then sends it to ${described.join('; ')}`, MAX_LINE) };
|
|
922
967
|
});
|
|
923
968
|
}
|
|
969
|
+
const HEREDOC_OPENER = /(?:^|[^<])<<-?[ \t]*(?:'[^'\n]*'|"[^"\n]*"|\\?[^\s;&|<>()'"]+)/g;
|
|
970
|
+
/**
|
|
971
|
+
* A heredoc body is the input of the step that opens it, not steps of its own:
|
|
972
|
+
* drop every body (and its delimiter line) and hand each opener step its body.
|
|
973
|
+
*/
|
|
974
|
+
function stepsWithHeredocs(command) {
|
|
975
|
+
const heredocs = (0, deterministicGuard_1.posixHeredocs)(command);
|
|
976
|
+
if (!heredocs.length)
|
|
977
|
+
return splitCommandSteps(command).map(segment => ({ segment, heredocs: [] }));
|
|
978
|
+
let code = '';
|
|
979
|
+
let at = 0;
|
|
980
|
+
for (const heredoc of heredocs) {
|
|
981
|
+
code += command.slice(at, heredoc.bodyStart);
|
|
982
|
+
at = Math.min(command.length, heredoc.end + 1);
|
|
983
|
+
}
|
|
984
|
+
code += command.slice(at);
|
|
985
|
+
let next = 0;
|
|
986
|
+
return splitCommandSteps(code).map(segment => {
|
|
987
|
+
const opened = segment.replace(/"(?:[^"\\]|\\.)*"|'[^']*'/g, m => (/<</.test(m) ? m : '""')).match(HEREDOC_OPENER) || [];
|
|
988
|
+
const mine = heredocs.slice(next, next + opened.length);
|
|
989
|
+
next += opened.length;
|
|
990
|
+
return { segment, heredocs: mine };
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
/** A long path shortened from the front: the file name is what the reader needs. */
|
|
994
|
+
function shortPath(path, max = 60) {
|
|
995
|
+
if (path.length <= max)
|
|
996
|
+
return path;
|
|
997
|
+
const parts = path.split(/[\\/]/);
|
|
998
|
+
let out = parts.pop() || '';
|
|
999
|
+
while (parts.length && out.length + parts[parts.length - 1].length + 3 <= max)
|
|
1000
|
+
out = `${parts.pop()}/${out}`;
|
|
1001
|
+
return `…/${out}`.slice(0, max + 2);
|
|
1002
|
+
}
|
|
1003
|
+
function lineCount(body) {
|
|
1004
|
+
return body ? body.split('\n').length : 0;
|
|
1005
|
+
}
|
|
1006
|
+
/** What a step does with the heredoc bodies it opens. */
|
|
1007
|
+
function describeHeredocStep(segment, heredocs, fns) {
|
|
1008
|
+
const [heredoc] = heredocs;
|
|
1009
|
+
const lines = lineCount(heredoc.body);
|
|
1010
|
+
const size = `${lines}-line`;
|
|
1011
|
+
const masked = segment.replace(/"(?:[^"\\]|\\.)*"|'[^']*'/g, m => m.replace(/[>|]/g, '_'));
|
|
1012
|
+
const target = /(?:^|[^>0-9-])>{1,2}\s*("[^"]*"|'[^']*'|[^\s&|;<]+)/.exec(masked);
|
|
1013
|
+
const file = target ? unquote(segment.slice(target.index + target[0].length - target[1].length, target.index + target[0].length)) : '';
|
|
1014
|
+
const words = tokens(segment).filter(t => !/^<</.test(t));
|
|
1015
|
+
const program = (words[0] || '').toLowerCase().replace(/^.*[\\/]/, '');
|
|
1016
|
+
const { kind, program: reader } = heredoc.receiver;
|
|
1017
|
+
const into = program === 'cat' ? file : program === 'tee' ? (file || words.slice(1).find(t => !t.startsWith('-')) || '') : '';
|
|
1018
|
+
// Writing the body to a file is this step; a later step that runs the file says so itself.
|
|
1019
|
+
if (into && !/\|/.test(masked)) {
|
|
1020
|
+
const what = kind === 'shell' ? `a ${size} shell script` : kind === 'language' ? `a ${size} ${reader} script` : `${lines} line${lines === 1 ? '' : 's'} of text`;
|
|
1021
|
+
return { segment, text: shortValue(`Writes ${what} to ${shortPath(into)}`, MAX_LINE), known: true };
|
|
1022
|
+
}
|
|
1023
|
+
if (kind === 'shell')
|
|
1024
|
+
return { segment, text: shortValue(`Runs a ${size} script with ${reader || program}`, MAX_LINE), known: true };
|
|
1025
|
+
if (kind === 'language')
|
|
1026
|
+
return { segment, text: shortValue(`Runs a ${size} ${reader} script`, MAX_LINE), known: true };
|
|
1027
|
+
const base = describeOne(segment.replace(HEREDOC_OPENER, m => m.replace(/<<[\s\S]*$/, '')), undefined, fns);
|
|
1028
|
+
return { ...base, segment, text: shortValue(`${base.text} (given ${lines} line${lines === 1 ? '' : 's'} of input text)`, MAX_LINE) };
|
|
1029
|
+
}
|
|
924
1030
|
/** One plain-words line per step, in order. */
|
|
925
1031
|
function describeShellCommand(command) {
|
|
926
|
-
const
|
|
1032
|
+
const parts = stepsWithHeredocs(command);
|
|
1033
|
+
const steps = parts.map(part => part.segment);
|
|
927
1034
|
const fns = localFunctions(command);
|
|
928
|
-
const described =
|
|
1035
|
+
const described = parts.map(({ segment, heredocs }) => {
|
|
929
1036
|
try {
|
|
930
|
-
return describeOne(segment, undefined, fns);
|
|
1037
|
+
return heredocs.length ? describeHeredocStep(segment, heredocs, fns) : describeOne(segment, undefined, fns);
|
|
931
1038
|
}
|
|
932
1039
|
catch {
|
|
933
1040
|
return { segment, text: 'Runs a command the grammar does not recognise', known: false };
|
package/dist/commands/daemon.js
CHANGED
|
@@ -73,6 +73,7 @@ const discoverPaths_1 = require("./discoverPaths");
|
|
|
73
73
|
const localSafetySnapshot_1 = require("../localSafetySnapshot");
|
|
74
74
|
const policyRefresh_1 = require("../policyRefresh");
|
|
75
75
|
const telemetry_1 = require("../telemetry");
|
|
76
|
+
const identityRecovery_1 = require("../identityRecovery");
|
|
76
77
|
const notify_1 = require("../notify");
|
|
77
78
|
const integrity_1 = require("../integrity");
|
|
78
79
|
const protectionMaintenance_1 = require("../protectionMaintenance");
|
|
@@ -1505,6 +1506,7 @@ async function runDaemon(args, config) {
|
|
|
1505
1506
|
// flush lock, which drops the daemon marker specifically (hook flushes do
|
|
1506
1507
|
// not set `daemon: true`), so retrying soon is right in that case too.
|
|
1507
1508
|
let reported = false;
|
|
1509
|
+
let refusal;
|
|
1508
1510
|
try {
|
|
1509
1511
|
let integrity = (0, integrity_1.getLocalIntegrityReport)({ requireDaemon: true });
|
|
1510
1512
|
// Recovery outside our repair pass (for example a manual fix) also clears
|
|
@@ -1568,6 +1570,8 @@ async function runDaemon(args, config) {
|
|
|
1568
1570
|
: undefined,
|
|
1569
1571
|
});
|
|
1570
1572
|
reported = result !== null;
|
|
1573
|
+
if (!reported)
|
|
1574
|
+
refusal = (0, telemetry_1.getLastFlushFailure)();
|
|
1571
1575
|
if (result && unreportedCrash)
|
|
1572
1576
|
(0, daemonForensics_1.markPostmortemReported)();
|
|
1573
1577
|
if (result && result.accepted > 0)
|
|
@@ -1583,10 +1587,26 @@ async function runDaemon(args, config) {
|
|
|
1583
1587
|
await verifyPendingUpgrade();
|
|
1584
1588
|
// Surface the miss to the caller (poll loop / resume path) so it retries in
|
|
1585
1589
|
// seconds. Nothing else can tell them: liveness has exactly one writer.
|
|
1586
|
-
if (!reported)
|
|
1587
|
-
|
|
1590
|
+
if (!reported) {
|
|
1591
|
+
// 409 MACHINE_NOT_ENROLLED: the id sent is not the one this shield is
|
|
1592
|
+
// enrolled under — ask the backend for it; the retry then lands.
|
|
1593
|
+
await (0, identityRecovery_1.recoverIdentityAfterRefusal)({ failure: refusal, apiUrl: creds.apiUrl, shieldId: creds.shieldId, shieldKey: creds.shieldKey, log })
|
|
1594
|
+
.catch(() => false);
|
|
1595
|
+
throw new Error((0, identityRecovery_1.describeHeartbeatMiss)(refusal));
|
|
1596
|
+
}
|
|
1588
1597
|
};
|
|
1589
1598
|
// --- boot ---------------------------------------------------------------
|
|
1599
|
+
// Settle the machine id before anything reports: the only place a pinned
|
|
1600
|
+
// but unconfirmed fallback id may be re-checked against the OS (#1853).
|
|
1601
|
+
try {
|
|
1602
|
+
const id = (0, machineIdentity_1.reconcileMachineIdentity)('heal');
|
|
1603
|
+
log(`Machine id ${(0, machineIdentity_1.shortMachineId)(id.machineId)} (${id.source}${id.confirmed ? ', backend-confirmed' : ''}${id.probed ? `, native read ${id.probeError ? `failed: ${id.probeError}` : 'ok'}` : ''}).`
|
|
1604
|
+
+ (id.healed ? ` Healed from stale fallback id ${(0, machineIdentity_1.shortMachineId)(id.healed.from)}.` : '')
|
|
1605
|
+
+ (id.conflict ? ` Native id ${(0, machineIdentity_1.shortMachineId)(id.conflict.native)} disagrees with the confirmed id — kept the confirmed one (machine_id_conflict).` : ''));
|
|
1606
|
+
}
|
|
1607
|
+
catch (error) {
|
|
1608
|
+
log(`Machine id reconcile failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1609
|
+
}
|
|
1590
1610
|
// Self-heal autostart + watchdog tasks: machines installed by older versions
|
|
1591
1611
|
// (or where task creation failed once) must converge without a reinstall.
|
|
1592
1612
|
ensureWindowsAutostartHealthy(log);
|
|
@@ -1625,7 +1645,7 @@ async function runDaemon(args, config) {
|
|
|
1625
1645
|
// the network is up (captive portal, VPN, resumed in a lift) is routine, and
|
|
1626
1646
|
// the heartbeat loop below retries within seconds.
|
|
1627
1647
|
await heartbeat().catch(error => {
|
|
1628
|
-
log(`First heartbeat
|
|
1648
|
+
log(`First heartbeat failed: ${error?.message || String(error)}. The heartbeat loop will retry shortly.`);
|
|
1629
1649
|
});
|
|
1630
1650
|
// Claude Desktop chat guard (Windows, advisory): Claude Desktop's regular
|
|
1631
1651
|
// chat has no hook and never hits an MCP server, so it is the one machine
|
|
@@ -176,8 +176,8 @@ exports.DEMO_SCENARIOS = [
|
|
|
176
176
|
id: 'recursive-delete',
|
|
177
177
|
title: 'Recursive delete of a project folder',
|
|
178
178
|
event: 'shell',
|
|
179
|
-
payload: { command: 'rm -rf ~/projects
|
|
180
|
-
why: '
|
|
179
|
+
payload: { command: 'rm -rf ~/projects' },
|
|
180
|
+
why: 'A recursive delete of a top-level home folder is classified as DELETE and can be governed by policy. Nested work paths (`~/projects/payments`) are workspace writes, not DELETE.',
|
|
181
181
|
},
|
|
182
182
|
];
|
|
183
183
|
function tildify(filePath) {
|