fullcourtdefense-cli 1.35.0 → 1.35.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.
- package/dist/actionPolicyEngine.js +30 -5
- package/dist/approvalPrompt.d.ts +25 -0
- package/dist/approvalPrompt.js +51 -0
- package/dist/detectorBaseline.d.ts +2 -2
- package/dist/detectorBaseline.js +2 -2
- package/dist/devConfirm.d.ts +3 -1
- package/dist/devConfirm.js +132 -27
- package/dist/version.json +1 -1
- package/package.json +1 -1
|
@@ -6814,11 +6814,13 @@ function shellVerbWords(command) {
|
|
|
6814
6814
|
let lead = "";
|
|
6815
6815
|
let first = true;
|
|
6816
6816
|
let scriptArgs = false;
|
|
6817
|
+
let previousBare = "";
|
|
6817
6818
|
return shellCommandWords(command).split(/\s+/).filter(Boolean).map((token) => {
|
|
6818
6819
|
if (token === ";") {
|
|
6819
6820
|
lead = "";
|
|
6820
6821
|
first = true;
|
|
6821
6822
|
scriptArgs = false;
|
|
6823
|
+
previousBare = "";
|
|
6822
6824
|
return token;
|
|
6823
6825
|
}
|
|
6824
6826
|
if (scriptArgs) return "";
|
|
@@ -6838,12 +6840,19 @@ function shellVerbWords(command) {
|
|
|
6838
6840
|
}
|
|
6839
6841
|
const dot = word.startsWith(".") ? "." : "";
|
|
6840
6842
|
if (dot && !DOT_COMMAND_PROGRAMS.has(lead)) return "";
|
|
6843
|
+
if (lead === "gh" && GH_CONFIG_RESOURCES.has(previousBare) && /^(?:delete|remove)$/i.test(word)) {
|
|
6844
|
+
const glued = `${previousBare}${word.toLowerCase()}`;
|
|
6845
|
+
previousBare = "";
|
|
6846
|
+
return glued;
|
|
6847
|
+
}
|
|
6848
|
+
if (!flag) previousBare = word.toLowerCase();
|
|
6841
6849
|
const [head, ...rest] = word.replace(/^\./, "").split(/[-_]+/).filter(Boolean);
|
|
6842
6850
|
if (!head) return "";
|
|
6843
6851
|
return `${flag ? flag[1] : dot}${head}${rest.length ? ` ${rest.join("")}` : ""}`;
|
|
6844
6852
|
}).filter(Boolean).join(" ");
|
|
6845
6853
|
}
|
|
6846
6854
|
var VCS_PROGRAMS = /* @__PURE__ */ new Set(["git", "gh", "hg", "svn", "glab"]);
|
|
6855
|
+
var GH_CONFIG_RESOURCES = /* @__PURE__ */ new Set(["secret", "variable", "cache", "alias", "label", "ssh-key", "gpg-key"]);
|
|
6847
6856
|
var SCRIPT_RUNNER_LEADS = /* @__PURE__ */ new Set([
|
|
6848
6857
|
"bash",
|
|
6849
6858
|
"sh",
|
|
@@ -7346,13 +7355,14 @@ function temporaryFileSyntax(text) {
|
|
|
7346
7355
|
return text.replace(/"\$env:(?:TEMP|TMP)([\\/][A-Za-z0-9_ ./\\-]+)"/gi, '"fcd-temporary$1"').replace(/(?<=^|\s)\$env:(?:TEMP|TMP)([\\/][A-Za-z0-9_./\\-]+)(?=\s|$)/gi, '"fcd-temporary$1"').replace(/\(Join-Path\s+\$env:(?:TEMP|TMP)\s+'([A-Za-z0-9_ ./\\-]+)'\)/gi, '"fcd-temporary/$1"');
|
|
7347
7356
|
}
|
|
7348
7357
|
var GH_FLAG_VALUE_RE = /^(?:'[^'$`\r\n]*'|"[^"$`\r\n]*"|[^\s'"$`|;&(){}<>]+)$/;
|
|
7358
|
+
var GH_PROSE_VALUE_RE = /^(?:'[^'$`]*'|"[^"$`]*"|[^\s'"$`|;&(){}<>]+)$/;
|
|
7349
7359
|
function ghMetadataWriteBodyFile(segment) {
|
|
7350
7360
|
return parseGhMetadataWrite(segment)?.bodyFile;
|
|
7351
7361
|
}
|
|
7352
7362
|
function parseGhMetadataWrite(segment) {
|
|
7353
|
-
const ghBody = /^(gh\s+(?:pr\s+create|issue\s+create|pr\s+(?:comment|edit)\s+\d+|issue\s+(?:comment|edit)\s+\d+)\s+)(
|
|
7363
|
+
const ghBody = /^(gh\s+(?:pr\s+create|issue\s+create|pr\s+(?:comment|edit)\s+\d+|issue\s+(?:comment|edit)\s+\d+)\s+)([\s\S]+?)((?:\s+2>&1)?(?:\s*\|\s*Select-Object\s+-Last\s+\d{1,3})?)$/i.exec(segment);
|
|
7354
7364
|
if (!ghBody) return void 0;
|
|
7355
|
-
const tokens = ghBody[2].match(/'[^'
|
|
7365
|
+
const tokens = ghBody[2].match(/'[^']*'|"[^"]*"|[^\s'"]+/g) || [];
|
|
7356
7366
|
if (tokens.join("").replace(/\s/g, "") !== ghBody[2].replace(/\s/g, "")) return void 0;
|
|
7357
7367
|
let bodyFile;
|
|
7358
7368
|
let bodySeen = false;
|
|
@@ -7360,7 +7370,10 @@ function parseGhMetadataWrite(segment) {
|
|
|
7360
7370
|
for (let i2 = 0; i2 < tokens.length; i2 += 2) {
|
|
7361
7371
|
const flag = tokens[i2];
|
|
7362
7372
|
const value = tokens[i2 + 1];
|
|
7363
|
-
if (!/^--(?:base|head|title|label|assignee|milestone|reviewer|body-file|body)$/i.test(flag) || !value
|
|
7373
|
+
if (!/^--(?:base|head|title|label|assignee|milestone|reviewer|body-file|body)$/i.test(flag) || !value) return void 0;
|
|
7374
|
+
const literal2 = (/^--(?:body|title)$/i.test(flag) ? GH_PROSE_VALUE_RE : GH_FLAG_VALUE_RE).test(value);
|
|
7375
|
+
const selectorVariable = !/^--(?:body|title|body-file)$/i.test(flag) && /^\$[A-Za-z_]\w*$/.test(value);
|
|
7376
|
+
if (!literal2 && !selectorVariable) return void 0;
|
|
7364
7377
|
if (/^--body-file$/i.test(flag)) {
|
|
7365
7378
|
if (bodySeen) return void 0;
|
|
7366
7379
|
bodySeen = true;
|
|
@@ -7376,6 +7389,11 @@ function parseGhMetadataWrite(segment) {
|
|
|
7376
7389
|
return { bodyFile, masked: `${ghBody[1]}${kept.join(" ")}${ghBody[3]}` };
|
|
7377
7390
|
}
|
|
7378
7391
|
function withoutGhMetadataProse(segment) {
|
|
7392
|
+
const assigned = /^(\s*\$[A-Za-z_]\w*\s*=\s*)([\s\S]+)$/.exec(segment);
|
|
7393
|
+
if (assigned) {
|
|
7394
|
+
const masked = parseGhMetadataWrite(assigned[2])?.masked;
|
|
7395
|
+
return masked === void 0 ? segment : `${assigned[1]}${masked}`;
|
|
7396
|
+
}
|
|
7379
7397
|
return parseGhMetadataWrite(segment)?.masked ?? segment;
|
|
7380
7398
|
}
|
|
7381
7399
|
function shellUrlActionText(command) {
|
|
@@ -7954,7 +7972,8 @@ function provenCompoundDownload(command, detectedUrl) {
|
|
|
7954
7972
|
return segments.every((segment) => {
|
|
7955
7973
|
const statusRead = /^\(((?:Invoke-WebRequest|iwr)\s+[^\r\n]+)\)\.(?:StatusCode|StatusDescription|RawContentLength)$/i.exec(segment);
|
|
7956
7974
|
if (statusRead) segment = statusRead[1];
|
|
7957
|
-
|
|
7975
|
+
const withoutWriteOut = segment.replace(/(\s(?:-w|--write-out)\s+)(?:"[^"\r\n$`@]*"|'[^'\r\n$`@]*')/g, '$1"fcd-write-out"');
|
|
7976
|
+
if (/[$`{}()<>;&]/.test(withoutWriteOut)) return false;
|
|
7958
7977
|
const pipeline = splitShellSegments(segment, true);
|
|
7959
7978
|
if (pipeline.slice(1).some((part) => !isPowerShellReadFormatter(part))) return false;
|
|
7960
7979
|
const raw = pipeline[0];
|
|
@@ -7968,7 +7987,13 @@ function provenCompoundDownload(command, detectedUrl) {
|
|
|
7968
7987
|
let urls = 0;
|
|
7969
7988
|
for (let i2 = 0; i2 < tokens.length; i2++) {
|
|
7970
7989
|
const token = tokens[i2];
|
|
7971
|
-
if (isCurl && /^(
|
|
7990
|
+
if (isCurl && /^(?:-m|--max-time|--connect-timeout)$/.test(token) && /^\d+(?:\.\d+)?$/.test(tokens[i2 + 1] || "")) {
|
|
7991
|
+
i2++;
|
|
7992
|
+
continue;
|
|
7993
|
+
}
|
|
7994
|
+
if (isCurl && /^(?:-w|--write-out)$/.test(token)) {
|
|
7995
|
+
const format = tokens[i2 + 1] || "";
|
|
7996
|
+
if (!format || /^@/.test(format) || /[$`]/.test(format)) return false;
|
|
7972
7997
|
i2++;
|
|
7973
7998
|
continue;
|
|
7974
7999
|
}
|
package/dist/approvalPrompt.d.ts
CHANGED
|
@@ -5,7 +5,32 @@ export interface ApprovalMessageOptions {
|
|
|
5
5
|
alwaysScopeLabel?: string;
|
|
6
6
|
/** How long it holds: "30 days" (default) or "7 days or 20 uses". */
|
|
7
7
|
alwaysLimitsLabel?: string;
|
|
8
|
+
/** Visual variant. Ask is the interactive dialog; approval/block share the card, not extra buttons. */
|
|
9
|
+
tone?: DialogTone;
|
|
8
10
|
}
|
|
11
|
+
/** Which policy outcome this window is showing. Icons and copy follow this; buttons do not. */
|
|
12
|
+
export type DialogTone = 'ask' | 'approval' | 'block';
|
|
13
|
+
export interface NativeDialogView {
|
|
14
|
+
tone: DialogTone;
|
|
15
|
+
badge: string;
|
|
16
|
+
headline: string;
|
|
17
|
+
subtitle: string;
|
|
18
|
+
kind?: string;
|
|
19
|
+
action?: string;
|
|
20
|
+
policy?: string;
|
|
21
|
+
scope?: string;
|
|
22
|
+
agent?: string;
|
|
23
|
+
ide?: string;
|
|
24
|
+
tool?: string;
|
|
25
|
+
why: string[];
|
|
26
|
+
next: string[];
|
|
27
|
+
footer: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Layout model for the branded OS dialog. Same facts as the compact prompt;
|
|
31
|
+
* skips mockup-only chrome (other-outcome chips, Request approval, project tags).
|
|
32
|
+
*/
|
|
33
|
+
export declare function nativeDialogView(reason: string, options?: ApprovalMessageOptions): NativeDialogView;
|
|
9
34
|
/** Window title: the kind of action and the policy that stopped it, by name. */
|
|
10
35
|
export declare function nativeApprovalTitle(reason: string): string;
|
|
11
36
|
export declare function nativeApprovalMessage(reason: string, options?: ApprovalMessageOptions): string;
|
package/dist/approvalPrompt.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.nativeDialogView = nativeDialogView;
|
|
3
4
|
exports.nativeApprovalTitle = nativeApprovalTitle;
|
|
4
5
|
exports.nativeApprovalMessage = nativeApprovalMessage;
|
|
5
6
|
/** Compact OS prompt. The complete decision remains in the IDE response and audit. */
|
|
@@ -54,6 +55,56 @@ function promptFacts(reason) {
|
|
|
54
55
|
}
|
|
55
56
|
return facts;
|
|
56
57
|
}
|
|
58
|
+
function sentences(text) {
|
|
59
|
+
return text.split(/(?<=\.)\s+/).map(part => part.trim()).filter(Boolean);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Layout model for the branded OS dialog. Same facts as the compact prompt;
|
|
63
|
+
* skips mockup-only chrome (other-outcome chips, Request approval, project tags).
|
|
64
|
+
*/
|
|
65
|
+
function nativeDialogView(reason, options = {}) {
|
|
66
|
+
const f = promptFacts(reason);
|
|
67
|
+
const tone = options.tone || 'ask';
|
|
68
|
+
const why = [
|
|
69
|
+
...(f.because ? sentences(line(f.because, 400)) : []),
|
|
70
|
+
f.note ? line(f.note, 180) : '',
|
|
71
|
+
f.fallback ? line(f.fallback, 400) : '',
|
|
72
|
+
].filter(Boolean);
|
|
73
|
+
const next = tone === 'block'
|
|
74
|
+
? ['This action is stopped.', 'An admin can change the rule in the console.']
|
|
75
|
+
: tone === 'approval'
|
|
76
|
+
? ['An admin or manager can approve it in the console.', 'Closing this window does not approve it.']
|
|
77
|
+
: [
|
|
78
|
+
'Allow once lets only this action continue.',
|
|
79
|
+
'Deny or close keeps it blocked.',
|
|
80
|
+
options.allowAlways && options.alwaysScopeLabel
|
|
81
|
+
? `Always allow covers ${line(options.alwaysScopeLabel, 180)} for ${options.alwaysLimitsLabel || '30 days'} on this machine, under this rule as it is written today.`
|
|
82
|
+
: '',
|
|
83
|
+
].filter(Boolean);
|
|
84
|
+
const footer = f.policy && !f.fallback
|
|
85
|
+
? `Full details: the IDE message and this machine’s Activity. Console → Action Policies → ${f.localSafety ? 'Source: Local Safety → ' : ''}"${line(f.policy, 80)}".`
|
|
86
|
+
: 'Full details are in the IDE message and this machine’s Activity in FullCourtDefense.';
|
|
87
|
+
return {
|
|
88
|
+
tone,
|
|
89
|
+
badge: tone === 'block' ? 'Blocked' : tone === 'approval' ? 'Approval required' : 'Ask the developer',
|
|
90
|
+
headline: tone === 'block' ? 'Action blocked' : tone === 'approval' ? 'Action requires approval' : 'Action needs your OK',
|
|
91
|
+
subtitle: tone === 'block'
|
|
92
|
+
? 'A security policy stopped this action. It did not run.'
|
|
93
|
+
: tone === 'approval'
|
|
94
|
+
? 'A security policy paused this action for an admin or manager.'
|
|
95
|
+
: 'A security policy paused this action. Review the details and choose what to do next.',
|
|
96
|
+
kind: f.kind,
|
|
97
|
+
action: f.action ? line(f.action, 280) : undefined,
|
|
98
|
+
policy: f.policy ? line(f.policy, 160) : undefined,
|
|
99
|
+
scope: f.scope ? line(f.scope, 220) : undefined,
|
|
100
|
+
agent: f.agent ? line(f.agent, 80) : undefined,
|
|
101
|
+
ide: f.ide ? line(f.ide, 40) : undefined,
|
|
102
|
+
tool: f.tool ? line(f.tool, 230) : undefined,
|
|
103
|
+
why: why.length ? why : ['A policy matched this action.'],
|
|
104
|
+
next,
|
|
105
|
+
footer,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
57
108
|
/** Window title: the kind of action and the policy that stopped it, by name. */
|
|
58
109
|
function nativeApprovalTitle(reason) {
|
|
59
110
|
const f = promptFacts(reason);
|