@xano/cli 1.3.0 → 1.3.1-beta.0
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/README.md +269 -1
- package/dist/base-command.d.ts +12 -1
- package/dist/base-command.js +36 -22
- package/dist/commands/branch/set_live/index.d.ts +22 -0
- package/dist/commands/branch/set_live/index.js +60 -1
- package/dist/commands/ephemeral/push/index.js +6 -1
- package/dist/commands/function/create/index.d.ts +15 -0
- package/dist/commands/function/create/index.js +40 -0
- package/dist/commands/function/edit/index.d.ts +15 -0
- package/dist/commands/function/edit/index.js +40 -0
- package/dist/commands/policy/catalogue/index.d.ts +15 -0
- package/dist/commands/policy/catalogue/index.js +29 -0
- package/dist/commands/policy/create/index.d.ts +18 -0
- package/dist/commands/policy/create/index.js +39 -0
- package/dist/commands/policy/delete/index.d.ts +19 -0
- package/dist/commands/policy/delete/index.js +61 -0
- package/dist/commands/policy/evaluate/index.d.ts +32 -0
- package/dist/commands/policy/evaluate/index.js +71 -0
- package/dist/commands/policy/list/index.d.ts +14 -0
- package/dist/commands/policy/list/index.js +21 -0
- package/dist/commands/policy/parse/index.d.ts +19 -0
- package/dist/commands/policy/parse/index.js +18 -0
- package/dist/commands/policy/publish/index.d.ts +20 -0
- package/dist/commands/policy/publish/index.js +38 -0
- package/dist/commands/policy/runs/index.d.ts +54 -0
- package/dist/commands/policy/runs/index.js +273 -0
- package/dist/commands/policy/status/index.d.ts +21 -0
- package/dist/commands/policy/status/index.js +54 -0
- package/dist/commands/release/create/index.js +7 -0
- package/dist/commands/release/deploy/index.d.ts +28 -0
- package/dist/commands/release/deploy/index.js +90 -6
- package/dist/commands/release/import/index.js +5 -1
- package/dist/commands/release/list/index.d.ts +7 -0
- package/dist/commands/release/list/index.js +54 -10
- package/dist/commands/release/push/index.js +19 -1
- package/dist/commands/sandbox/push/index.js +6 -1
- package/dist/commands/skills/pull/index.d.ts +30 -0
- package/dist/commands/skills/pull/index.js +118 -0
- package/dist/commands/tenant/create/index.d.ts +3 -0
- package/dist/commands/tenant/create/index.js +21 -0
- package/dist/commands/tenant/deploy_release/index.d.ts +40 -0
- package/dist/commands/tenant/deploy_release/index.js +186 -4
- package/dist/commands/tenant/edit/index.d.ts +2 -0
- package/dist/commands/tenant/edit/index.js +39 -4
- package/dist/commands/tenant_deploy_request/bypass/index.d.ts +31 -0
- package/dist/commands/tenant_deploy_request/bypass/index.js +109 -0
- package/dist/commands/tenant_deploy_request/create/index.d.ts +21 -0
- package/dist/commands/tenant_deploy_request/create/index.js +108 -0
- package/dist/commands/tenant_deploy_request/edit/index.d.ts +22 -0
- package/dist/commands/tenant_deploy_request/edit/index.js +103 -0
- package/dist/commands/tenant_deploy_request/get/index.d.ts +25 -0
- package/dist/commands/tenant_deploy_request/get/index.js +116 -0
- package/dist/commands/tenant_deploy_request/list/index.d.ts +20 -0
- package/dist/commands/tenant_deploy_request/list/index.js +122 -0
- package/dist/commands/tenant_deploy_request/revision/index.d.ts +21 -0
- package/dist/commands/tenant_deploy_request/revision/index.js +83 -0
- package/dist/commands/tenant_deploy_request/set_status/index.d.ts +38 -0
- package/dist/commands/tenant_deploy_request/set_status/index.js +159 -0
- package/dist/commands/workspace/pull/index.d.ts +10 -0
- package/dist/commands/workspace/pull/index.js +70 -7
- package/dist/commands/workspace/push/index.d.ts +19 -0
- package/dist/commands/workspace/push/index.js +124 -5
- package/dist/policy-command.d.ts +90 -0
- package/dist/policy-command.js +94 -0
- package/dist/utils/document-parser.d.ts +10 -0
- package/dist/utils/document-parser.js +21 -0
- package/dist/utils/multidoc-push.d.ts +55 -2
- package/dist/utils/multidoc-push.js +103 -27
- package/dist/utils/policy/catalogue.d.ts +7 -0
- package/dist/utils/policy/catalogue.js +72 -0
- package/dist/utils/policy/errors.d.ts +12 -0
- package/dist/utils/policy/errors.js +61 -0
- package/dist/utils/policy/feedback.d.ts +68 -0
- package/dist/utils/policy/feedback.js +165 -0
- package/dist/utils/policy/findings.d.ts +23 -0
- package/dist/utils/policy/findings.js +65 -0
- package/dist/utils/policy/gate.d.ts +161 -0
- package/dist/utils/policy/gate.js +201 -0
- package/dist/utils/policy/goal.d.ts +46 -0
- package/dist/utils/policy/goal.js +95 -0
- package/dist/utils/policy/permission.d.ts +23 -0
- package/dist/utils/policy/permission.js +84 -0
- package/dist/utils/policy/push-gate.d.ts +5 -0
- package/dist/utils/policy/push-gate.js +29 -0
- package/dist/utils/policy/release.d.ts +85 -0
- package/dist/utils/policy/release.js +115 -0
- package/dist/utils/policy/request.d.ts +55 -0
- package/dist/utils/policy/request.js +115 -0
- package/dist/utils/policy/runs.d.ts +30 -0
- package/dist/utils/policy/runs.js +101 -0
- package/dist/utils/policy/status.d.ts +47 -0
- package/dist/utils/policy/status.js +117 -0
- package/dist/utils/policy/types.d.ts +185 -0
- package/dist/utils/policy/types.js +1 -0
- package/oclif.manifest.json +6132 -3133
- package/package.json +7 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Keep backend implementation details out of ordinary command errors. */
|
|
2
|
+
function withoutStack(text) {
|
|
3
|
+
return text.split(/\n\s*(?:Stack trace:|#\d+\s|at\s|file:\s*\/)/i, 1)[0];
|
|
4
|
+
}
|
|
5
|
+
/** Where a parse error is: the payload's 1-based `line` and `col`, printed as served, and the offending text. */
|
|
6
|
+
function positionLine(payload) {
|
|
7
|
+
const text = [payload.error_line, payload.error_snippet, payload.snippet]
|
|
8
|
+
.find(value => typeof value === 'string' && value.trim() !== '');
|
|
9
|
+
const where = typeof payload.line === 'number' && Number.isFinite(payload.line)
|
|
10
|
+
? `line ${payload.line}${typeof payload.col === 'number' && Number.isFinite(payload.col) ? `, col ${payload.col}` : ''}`
|
|
11
|
+
: '';
|
|
12
|
+
if (!where && text === undefined)
|
|
13
|
+
return '';
|
|
14
|
+
return `\n ${where ? `at ${where}` : 'at'}${text === undefined ? '' : `: ${text}`}`;
|
|
15
|
+
}
|
|
16
|
+
/** An empty 404 from a branch-scoped request means the branch it named does not exist. */
|
|
17
|
+
function missingBranch(requestUrl) {
|
|
18
|
+
const url = new URL(requestUrl);
|
|
19
|
+
const workspace = url.pathname.match(/\/workspace\/([^/]+)/)?.[1];
|
|
20
|
+
const branch = url.searchParams.get('branch');
|
|
21
|
+
return workspace && branch ? `Branch "${branch}" was not found in workspace ${decodeURIComponent(workspace)}.` : '';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* What to do about a refusal of the request itself, keyed on the platform's `payload.code`:
|
|
25
|
+
*
|
|
26
|
+
* policy_unknown_check a rule names a check id this instance does not have
|
|
27
|
+
* policy_stale the policy changed after the command read it
|
|
28
|
+
*
|
|
29
|
+
* Permission refusals are explained by `policyPermissionGuidance`.
|
|
30
|
+
*/
|
|
31
|
+
const CODE_GUIDANCE = {
|
|
32
|
+
policy_stale: '\nThe policy changed after this command read it, so nothing was changed. Run the command again to act on its current version.',
|
|
33
|
+
policy_unknown_check: '\nRun `xano policy catalogue` to list every check id this instance has.',
|
|
34
|
+
};
|
|
35
|
+
/** Guidance appended to a refusal whose `payload.code` the CLI has advice for, or `''`. */
|
|
36
|
+
export function policyCodeGuidance(payload) {
|
|
37
|
+
const code = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload.code : undefined;
|
|
38
|
+
return typeof code === 'string' && Object.hasOwn(CODE_GUIDANCE, code) ? CODE_GUIDANCE[code] : '';
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A failed policy-route response as one message: the platform's code and message, and where a parse
|
|
42
|
+
* error is. Traces are dropped, and an empty 404 names the branch the request selected.
|
|
43
|
+
*/
|
|
44
|
+
export function describePolicyError(text, status, requestUrl) {
|
|
45
|
+
let data;
|
|
46
|
+
try {
|
|
47
|
+
data = JSON.parse(text.trim() || '{}');
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return { message: withoutStack(text) };
|
|
51
|
+
}
|
|
52
|
+
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
53
|
+
return { message: withoutStack(text) };
|
|
54
|
+
const { code, message: raw, payload } = data;
|
|
55
|
+
let message = typeof raw === 'string' ? withoutStack(raw) : '';
|
|
56
|
+
if (status === 404 && !message.trim())
|
|
57
|
+
message = missingBranch(requestUrl);
|
|
58
|
+
const headline = [typeof code === 'string' ? code : '', message].filter(Boolean).join(': ');
|
|
59
|
+
const position = payload && typeof payload === 'object' ? positionLine(payload) : '';
|
|
60
|
+
return { message: `${headline}${position}` || 'The server returned no message.', payload };
|
|
61
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { PolicyEvaluation, PolicyFinding, PolicyRuleResult, PolicySnapshotPolicy, PolicyVerdict, PushPolicyCheck } from './types.js';
|
|
2
|
+
/** 2 whenever the platform says a finding blocks, whatever the status; every other outcome is 0. */
|
|
3
|
+
export declare function policyExitCode(check?: PolicyVerdict): number;
|
|
4
|
+
/**
|
|
5
|
+
* The one warning line for feedback without a headline (`disabled`, `forbidden`, `unavailable`,
|
|
6
|
+
* `error`, an unknown status, or a pass or fail that does not say whether it blocks): the server's
|
|
7
|
+
* own status and message, or that none came back. `null` for headlined feedback.
|
|
8
|
+
*/
|
|
9
|
+
export declare function policyCheckWarning(check?: PolicyVerdict): null | string;
|
|
10
|
+
/**
|
|
11
|
+
* What happened to the policy documents a push carried. The preview says, per policy, whether it was
|
|
12
|
+
* created, updated or left unchanged. Without a preview only the number sent is known: the import
|
|
13
|
+
* response reports an unchanged policy exactly as it reports a saved one.
|
|
14
|
+
*/
|
|
15
|
+
export declare function policyDocumentSummary(preview: null | {
|
|
16
|
+
operations: Array<{
|
|
17
|
+
action: string;
|
|
18
|
+
name: string;
|
|
19
|
+
type: string;
|
|
20
|
+
}>;
|
|
21
|
+
}, sentPolicies: number): string[];
|
|
22
|
+
/** What a verdict is about: its findings, which of them block, and the rule results. */
|
|
23
|
+
export interface PolicyEvidence {
|
|
24
|
+
/** The ids of the findings that block. */
|
|
25
|
+
blocking: Set<string>;
|
|
26
|
+
/**
|
|
27
|
+
* When the answer lists only the first findings: how many there are in all, and how many block.
|
|
28
|
+
* The stored run (`runId`, 0 when nothing was stored) has them all.
|
|
29
|
+
*/
|
|
30
|
+
cut?: {
|
|
31
|
+
blocking: number;
|
|
32
|
+
findings: number;
|
|
33
|
+
runId: number;
|
|
34
|
+
};
|
|
35
|
+
/** In the platform's order: blocking first, then by severity. */
|
|
36
|
+
findings: PolicyFinding[];
|
|
37
|
+
results: PolicyRuleResult[];
|
|
38
|
+
/** The run's own `policies[]`, which names unnamed rules; a push answers none. */
|
|
39
|
+
snapshot: PolicySnapshotPolicy[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A push answers no run, so its `policy_check` carries the findings and results itself: the first
|
|
43
|
+
* 100 of each list, with `counts` and `total` saying what they are out of.
|
|
44
|
+
*/
|
|
45
|
+
export declare function pushEvidence(check?: PushPolicyCheck): PolicyEvidence;
|
|
46
|
+
/**
|
|
47
|
+
* An evaluation answers the run, and its verdict names the blocking findings by id. Its summary
|
|
48
|
+
* answer (`--summary`) lists the first findings, and its counts say what they are out of.
|
|
49
|
+
*/
|
|
50
|
+
export declare function evaluationEvidence(evaluation: PolicyEvaluation): PolicyEvidence;
|
|
51
|
+
/**
|
|
52
|
+
* The feedback on stdout: a headline with the server's message, then any findings, errors and
|
|
53
|
+
* warnings. Feedback without a headline is reported by `policyCheckWarning` instead.
|
|
54
|
+
*/
|
|
55
|
+
export declare function policySummary(check: PolicyVerdict | undefined, evidence: PolicyEvidence): string[];
|
|
56
|
+
/**
|
|
57
|
+
* The platform's one notice that a push or its dry run left policy files out
|
|
58
|
+
* (`policies_skipped.message`): a push to a remote tenant or to a tenant without a policy table, a
|
|
59
|
+
* `release push` by a caller without `workspace:policy` create and update (its release carries the
|
|
60
|
+
* live branch's policies instead), and any push while the Policies feature is off. `null` when it
|
|
61
|
+
* left none out.
|
|
62
|
+
*/
|
|
63
|
+
export declare function policiesSkippedNotice(answer: unknown): null | string;
|
|
64
|
+
/**
|
|
65
|
+
* The line for an evaluation that ran but was not recorded (the credential may read, not write), or
|
|
66
|
+
* `null`. A branch with nothing to evaluate stores nothing either, and its headline says so.
|
|
67
|
+
*/
|
|
68
|
+
export declare function notStoredLine(evaluation: PolicyEvaluation): null | string;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { findingLine, hasNoObjects, policyResultSummary, snapshotRules } from './findings.js';
|
|
2
|
+
/**
|
|
3
|
+
* Feedback printed as a headline: a pass or fail that says whether its findings block, or a branch
|
|
4
|
+
* with no active policy to evaluate. Everything else is reported by `policyCheckWarning`.
|
|
5
|
+
*/
|
|
6
|
+
function isHeadlined(check) {
|
|
7
|
+
if (check.status === 'not_applicable')
|
|
8
|
+
return true;
|
|
9
|
+
return ['fail', 'pass'].includes(check.status ?? '') && typeof check.blocking === 'boolean';
|
|
10
|
+
}
|
|
11
|
+
/** 2 whenever the platform says a finding blocks, whatever the status; every other outcome is 0. */
|
|
12
|
+
export function policyExitCode(check) {
|
|
13
|
+
return check?.blocking === true ? 2 : 0;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The one warning line for feedback without a headline (`disabled`, `forbidden`, `unavailable`,
|
|
17
|
+
* `error`, an unknown status, or a pass or fail that does not say whether it blocks): the server's
|
|
18
|
+
* own status and message, or that none came back. `null` for headlined feedback.
|
|
19
|
+
*/
|
|
20
|
+
export function policyCheckWarning(check) {
|
|
21
|
+
if (!check)
|
|
22
|
+
return 'Policy check: no policy feedback returned.';
|
|
23
|
+
if (isHeadlined(check))
|
|
24
|
+
return null;
|
|
25
|
+
const status = typeof check.status === 'string' && check.status.trim() !== '' ? check.status.trim() : 'unknown';
|
|
26
|
+
const message = typeof check.message === 'string' && check.message.trim() !== ''
|
|
27
|
+
? check.message.trim()
|
|
28
|
+
: (['fail', 'pass'].includes(status) ? 'the server did not say whether its findings block.' : 'no message returned.');
|
|
29
|
+
return `Policy check ${status}: ${message}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* What happened to the policy documents a push carried. The preview says, per policy, whether it was
|
|
33
|
+
* created, updated or left unchanged. Without a preview only the number sent is known: the import
|
|
34
|
+
* response reports an unchanged policy exactly as it reports a saved one.
|
|
35
|
+
*/
|
|
36
|
+
export function policyDocumentSummary(preview, sentPolicies) {
|
|
37
|
+
const operations = (preview?.operations ?? []).filter((op) => op.type === 'policy');
|
|
38
|
+
if (operations.length === 0) {
|
|
39
|
+
return sentPolicies > 0
|
|
40
|
+
? [`Policy documents: ${sentPolicies} sent without a preview, so which of them changed is not known`]
|
|
41
|
+
: [];
|
|
42
|
+
}
|
|
43
|
+
const named = (action) => operations.filter((op) => op.action === action).map((op) => op.name).sort();
|
|
44
|
+
const created = named('create');
|
|
45
|
+
const updated = named('update');
|
|
46
|
+
const unchanged = named('unchanged');
|
|
47
|
+
const parts = [
|
|
48
|
+
created.length > 0 && `${created.length} created (${created.join(', ')})`,
|
|
49
|
+
updated.length > 0 && `${updated.length} updated (${updated.join(', ')})`,
|
|
50
|
+
unchanged.length > 0 && `${unchanged.length} unchanged`,
|
|
51
|
+
].filter(Boolean);
|
|
52
|
+
return parts.length > 0 ? [`Policy documents: ${parts.join(', ')}`] : [];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A push answers no run, so its `policy_check` carries the findings and results itself: the first
|
|
56
|
+
* 100 of each list, with `counts` and `total` saying what they are out of.
|
|
57
|
+
*/
|
|
58
|
+
export function pushEvidence(check) {
|
|
59
|
+
return {
|
|
60
|
+
blocking: new Set((check?.blocking_findings ?? []).map(finding => finding.id ?? '').filter(Boolean)),
|
|
61
|
+
...(check?.truncated && check.counts
|
|
62
|
+
? { cut: { blocking: check.counts.blocking, findings: check.total ?? check.counts.findings, runId: check.run_id ?? 0 } }
|
|
63
|
+
: {}),
|
|
64
|
+
findings: check?.findings ?? [],
|
|
65
|
+
results: check?.results ?? [],
|
|
66
|
+
snapshot: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* An evaluation answers the run, and its verdict names the blocking findings by id. Its summary
|
|
71
|
+
* answer (`--summary`) lists the first findings, and its counts say what they are out of.
|
|
72
|
+
*/
|
|
73
|
+
export function evaluationEvidence(evaluation) {
|
|
74
|
+
const { counts } = evaluation;
|
|
75
|
+
return {
|
|
76
|
+
blocking: new Set(evaluation.policy_check?.blocking_finding_ids ?? []),
|
|
77
|
+
...(evaluation.truncated && counts
|
|
78
|
+
? { cut: { blocking: counts.blocking, findings: evaluation.total ?? counts.findings, runId: evaluation.stored === false ? 0 : (evaluation.id ?? 0) } }
|
|
79
|
+
: {}),
|
|
80
|
+
findings: evaluation.findings ?? [],
|
|
81
|
+
results: evaluation.results ?? [],
|
|
82
|
+
snapshot: evaluation.policies ?? [],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** A list's count, or `first N of M` when the answer lists only the first of them. */
|
|
86
|
+
function listed(count, all) {
|
|
87
|
+
return all !== undefined && all > count ? `first ${count} of ${all}` : String(count);
|
|
88
|
+
}
|
|
89
|
+
/** The headline's outcome. A pass whose rules inspected no object is never headlined as a plain pass. */
|
|
90
|
+
function outcome(check, results) {
|
|
91
|
+
if (check.status === 'fail')
|
|
92
|
+
return check.blocking ? 'fail (blocking findings)' : 'advisory findings (not blocking)';
|
|
93
|
+
if (check.status !== 'pass')
|
|
94
|
+
return check.status ?? '';
|
|
95
|
+
const unchecked = results.filter((result) => hasNoObjects(result)).length;
|
|
96
|
+
if (unchecked === 0)
|
|
97
|
+
return 'pass';
|
|
98
|
+
return unchecked === results.length
|
|
99
|
+
? 'pass, but no objects were checked'
|
|
100
|
+
: `pass, but ${unchecked} rule${unchecked === 1 ? '' : 's'} checked no objects`;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The feedback on stdout: a headline with the server's message, then any findings, errors and
|
|
104
|
+
* warnings. Feedback without a headline is reported by `policyCheckWarning` instead.
|
|
105
|
+
*/
|
|
106
|
+
export function policySummary(check, evidence) {
|
|
107
|
+
if (!check)
|
|
108
|
+
return [];
|
|
109
|
+
const lines = [];
|
|
110
|
+
if (isHeadlined(check)) {
|
|
111
|
+
lines.push(`Policy check: ${outcome(check, evidence.results)}`);
|
|
112
|
+
if (check.message)
|
|
113
|
+
lines.push(check.message);
|
|
114
|
+
}
|
|
115
|
+
const rules = snapshotRules(evidence.snapshot);
|
|
116
|
+
const { blocking, cut, findings } = evidence;
|
|
117
|
+
// A cut list is out of the counts; the ones the answer lists come first, blocking first.
|
|
118
|
+
const allBlocking = cut?.blocking;
|
|
119
|
+
const allAdvisory = cut ? cut.findings - cut.blocking : undefined;
|
|
120
|
+
if (cut)
|
|
121
|
+
lines.push(`Findings: ${cut.findings} (${cut.blocking} blocking, ${allAdvisory} advisory)`);
|
|
122
|
+
// The list splits only when some findings block and others do not.
|
|
123
|
+
const separated = blocking.size > 0 && findings.some(finding => !blocking.has(finding.id ?? ''));
|
|
124
|
+
if (separated) {
|
|
125
|
+
const blocked = findings.filter(finding => blocking.has(finding.id ?? ''));
|
|
126
|
+
const advisory = findings.filter(finding => !blocking.has(finding.id ?? ''));
|
|
127
|
+
lines.push(`Blocking findings (${listed(blocked.length, allBlocking)}) — active mandatory policies; a gate refuses a change that introduces one or changes its object:`, ...blocked.map(finding => findingLine(finding, rules)), `Advisory findings (${listed(advisory.length, allAdvisory)}) — reported, not blocking:`, ...advisory.map(finding => findingLine(finding, rules)));
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
if (blocking.size > 0 && findings.length > 0)
|
|
131
|
+
lines.push(`Blocking findings (${listed(findings.length, allBlocking)}) — active mandatory policies; a gate refuses a change that introduces one or changes its object:`);
|
|
132
|
+
// Without a headline (an `error` status, say) nothing else says what these findings are.
|
|
133
|
+
else if (findings.length > 0 && !isHeadlined(check))
|
|
134
|
+
lines.push(`Advisory findings (${listed(findings.length, allAdvisory)}) — reported, not blocking:`);
|
|
135
|
+
lines.push(...findings.map(finding => findingLine(finding, rules)));
|
|
136
|
+
}
|
|
137
|
+
if (cut) {
|
|
138
|
+
lines.push(`Listed: the first ${findings.length} of ${cut.findings}; ${cut.runId > 0
|
|
139
|
+
? `\`xano policy runs ${cut.runId}\` has them all.`
|
|
140
|
+
: 'this evaluation was not stored, so the rest cannot be listed.'}`);
|
|
141
|
+
}
|
|
142
|
+
lines.push(...policyResultSummary(evidence.results));
|
|
143
|
+
return lines;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The platform's one notice that a push or its dry run left policy files out
|
|
147
|
+
* (`policies_skipped.message`): a push to a remote tenant or to a tenant without a policy table, a
|
|
148
|
+
* `release push` by a caller without `workspace:policy` create and update (its release carries the
|
|
149
|
+
* live branch's policies instead), and any push while the Policies feature is off. `null` when it
|
|
150
|
+
* left none out.
|
|
151
|
+
*/
|
|
152
|
+
export function policiesSkippedNotice(answer) {
|
|
153
|
+
const skipped = answer && typeof answer === 'object' ? answer.policies_skipped : undefined;
|
|
154
|
+
const message = skipped && typeof skipped === 'object' ? skipped.message : undefined;
|
|
155
|
+
return typeof message === 'string' && message.trim() !== '' ? message.trim() : null;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The line for an evaluation that ran but was not recorded (the credential may read, not write), or
|
|
159
|
+
* `null`. A branch with nothing to evaluate stores nothing either, and its headline says so.
|
|
160
|
+
*/
|
|
161
|
+
export function notStoredLine(evaluation) {
|
|
162
|
+
if (evaluation.stored !== false || !['error', 'fail', 'pass'].includes(evaluation.policy_check?.status ?? ''))
|
|
163
|
+
return null;
|
|
164
|
+
return 'Not stored: this credential can run checks but not record runs, so `xano policy status` and `xano policy runs` will not show this one.';
|
|
165
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PolicyFinding, PolicyRuleResult, PolicySnapshotPolicy, PolicySnapshotRule } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The platform's naming rule, shared with Studio: the author's title, then the check's human
|
|
4
|
+
* label, then the rule id. Pass `label` from the run snapshot, or from the catalogue entry for
|
|
5
|
+
* `check` when the caller already holds the catalogue; never fetch one just to name a rule.
|
|
6
|
+
*/
|
|
7
|
+
export declare function policyRuleName(rule: PolicySnapshotRule): string;
|
|
8
|
+
/** Collision-free regardless of what a key or rule id contains. */
|
|
9
|
+
export declare const ruleKey: (policyKey?: string, ruleId?: string) => string;
|
|
10
|
+
export declare function snapshotRules(policies: PolicySnapshotPolicy[]): Map<string, PolicySnapshotRule>;
|
|
11
|
+
/**
|
|
12
|
+
* One finding line. The rule id leads it, because the check's label is shared by every
|
|
13
|
+
* rule that uses that check — with two rules of one policy failing, the labels alone do
|
|
14
|
+
* not tell them apart, and the id is what `--run-detail` and the JSON cite.
|
|
15
|
+
*/
|
|
16
|
+
export declare function findingLine(finding: PolicyFinding, rules: Map<string, PolicySnapshotRule>): string;
|
|
17
|
+
/** A completed rule with no objects checked, including older stored pass + checked 0 results. */
|
|
18
|
+
export declare function hasNoObjects(result: PolicyRuleResult): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Rule errors ("this rule could not run") and scope warnings ("part of your scope selected
|
|
21
|
+
* nothing") under their own headings, then the rules that inspected nothing.
|
|
22
|
+
*/
|
|
23
|
+
export declare function policyResultSummary(results?: PolicyRuleResult[]): string[];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platform's naming rule, shared with Studio: the author's title, then the check's human
|
|
3
|
+
* label, then the rule id. Pass `label` from the run snapshot, or from the catalogue entry for
|
|
4
|
+
* `check` when the caller already holds the catalogue; never fetch one just to name a rule.
|
|
5
|
+
*/
|
|
6
|
+
export function policyRuleName(rule) {
|
|
7
|
+
return rule.title?.trim() || rule.label?.trim() || rule.id?.trim() || '';
|
|
8
|
+
}
|
|
9
|
+
/** Collision-free regardless of what a key or rule id contains. */
|
|
10
|
+
export const ruleKey = (policyKey = '', ruleId = '') => JSON.stringify([policyKey, ruleId]);
|
|
11
|
+
export function snapshotRules(policies) {
|
|
12
|
+
return new Map(policies.flatMap(policy => (policy.rules ?? []).map(rule => [ruleKey(policy.key, rule.id), rule])));
|
|
13
|
+
}
|
|
14
|
+
/** A finding names its rule the way the platform does, falling back to what the run recorded. */
|
|
15
|
+
function findingName(finding, rules) {
|
|
16
|
+
const recorded = rules.get(ruleKey(finding.policy_key, finding.rule_id)) ?? {};
|
|
17
|
+
return policyRuleName({
|
|
18
|
+
...recorded,
|
|
19
|
+
id: finding.rule_id ?? recorded.id,
|
|
20
|
+
title: finding.rule_title || recorded.title,
|
|
21
|
+
}) || 'finding';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* One finding line. The rule id leads it, because the check's label is shared by every
|
|
25
|
+
* rule that uses that check — with two rules of one policy failing, the labels alone do
|
|
26
|
+
* not tell them apart, and the id is what `--run-detail` and the JSON cite.
|
|
27
|
+
*/
|
|
28
|
+
export function findingLine(finding, rules) {
|
|
29
|
+
// A rule the author left unnamed is called by its check's label, and only then by its id.
|
|
30
|
+
const rule = findingName(finding, rules);
|
|
31
|
+
const id = finding.rule_id?.trim();
|
|
32
|
+
const policy = finding.policy_title || finding.policy_key || 'policy';
|
|
33
|
+
const severity = finding.severity?.trim() ? ` [${finding.severity.trim()}]` : '';
|
|
34
|
+
return ` ${[id, id === rule ? '' : rule].filter(Boolean).join(' ')}${severity} (${policy}) ${finding.object?.type ?? ''} ${finding.object?.name ?? ''}: ${finding.message ?? ''}`;
|
|
35
|
+
}
|
|
36
|
+
/** A completed rule with no objects checked, including older stored pass + checked 0 results. */
|
|
37
|
+
export function hasNoObjects(result) {
|
|
38
|
+
return ['no_objects', 'pass'].includes(result.status ?? '') && (result.checked ?? 0) === 0;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Rule errors ("this rule could not run") and scope warnings ("part of your scope selected
|
|
42
|
+
* nothing") under their own headings, then the rules that inspected nothing.
|
|
43
|
+
*/
|
|
44
|
+
export function policyResultSummary(results = []) {
|
|
45
|
+
const where = (result) => `${result.policy_key ?? 'policy'} ${result.check_id ?? 'rule'}`;
|
|
46
|
+
const errors = results.filter(result => result.status === 'error')
|
|
47
|
+
.map(result => ` ${where(result)}: ${result.message ?? 'No diagnostic returned.'}`);
|
|
48
|
+
const warnings = results.flatMap(result => (result.warnings ?? []).map(warning => ` ${where(result)}: ${warning}`));
|
|
49
|
+
const lines = [];
|
|
50
|
+
if (errors.length > 0)
|
|
51
|
+
lines.push('Errors:', ...errors);
|
|
52
|
+
if (warnings.length > 0)
|
|
53
|
+
lines.push('Warnings:', ...warnings);
|
|
54
|
+
const checked = results.reduce((sum, result) => sum + (result.checked ?? 0), 0);
|
|
55
|
+
// When no rule checked anything, one sentence about the run says it all.
|
|
56
|
+
if (results.length > 0 && checked === 0) {
|
|
57
|
+
lines.push('No objects checked; this run does not demonstrate coverage.');
|
|
58
|
+
return lines;
|
|
59
|
+
}
|
|
60
|
+
const empty = results.filter(result => hasNoObjects(result));
|
|
61
|
+
if (empty.length > 0) {
|
|
62
|
+
lines.push('No objects checked (proves nothing about coverage):', ...empty.map(result => ` ${where(result)}: no objects checked`));
|
|
63
|
+
}
|
|
64
|
+
return lines;
|
|
65
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { ProfileConfig } from '../../base-command.js';
|
|
2
|
+
import type { PolicyFinding } from './types.js';
|
|
3
|
+
/** The release the tenant deploy gate judges. */
|
|
4
|
+
export interface GateRelease {
|
|
5
|
+
/** Whether the release was cut with its policies; one cut without them has no check. */
|
|
6
|
+
carried?: boolean;
|
|
7
|
+
counts?: null | {
|
|
8
|
+
advisory?: number;
|
|
9
|
+
blocking?: number;
|
|
10
|
+
errors?: number;
|
|
11
|
+
findings?: number;
|
|
12
|
+
};
|
|
13
|
+
id?: number;
|
|
14
|
+
name?: string;
|
|
15
|
+
/** The release's stored policy run, 0 when it has none. */
|
|
16
|
+
run_id?: number;
|
|
17
|
+
status?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The tenant deploy gate's answer: what `GET tenant/{name}/policy_gate` previews, and the payload of
|
|
21
|
+
* the 403 a refused deploy answers (with `code: policy_gate`). The gate judges the release alone, by
|
|
22
|
+
* its own policy check, so the same release gets the same verdict on every tenant; only `gated`
|
|
23
|
+
* differs. The findings come only to a credential that reads the workspace's policies.
|
|
24
|
+
*/
|
|
25
|
+
export interface PolicyGateAnswer {
|
|
26
|
+
can_override?: boolean;
|
|
27
|
+
code?: string;
|
|
28
|
+
/** The first of the release's blocking findings. */
|
|
29
|
+
findings?: PolicyFinding[];
|
|
30
|
+
/** `tenant_deploy`, or `tenant_approve` when an approval completes the deploy. */
|
|
31
|
+
gate?: string;
|
|
32
|
+
/** False for an ephemeral or sandbox tenant: the verdict is reported but never refuses. */
|
|
33
|
+
gated?: boolean;
|
|
34
|
+
message?: string;
|
|
35
|
+
override_denied?: boolean;
|
|
36
|
+
release?: GateRelease | null;
|
|
37
|
+
run_id?: number;
|
|
38
|
+
/** `pass`, `blocked`, `overridden`, `not_applicable`, `not_carried`, `disabled` or `unavailable`. */
|
|
39
|
+
status?: string;
|
|
40
|
+
/** Every blocking finding the release has, listed or not. */
|
|
41
|
+
total?: number;
|
|
42
|
+
truncated?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A gate that measures a change against the live branch (set-live, publish, a live-branch save): its
|
|
46
|
+
* blocking findings split by why they block.
|
|
47
|
+
*/
|
|
48
|
+
export interface LiveGateAnswer extends Omit<PolicyGateAnswer, 'gated' | 'release'> {
|
|
49
|
+
changed?: number;
|
|
50
|
+
existing?: number;
|
|
51
|
+
introduced?: number;
|
|
52
|
+
}
|
|
53
|
+
/** The refusal code a policy gate's 403 carries in `payload.code`. */
|
|
54
|
+
export declare const GATE_REFUSAL = "policy_gate";
|
|
55
|
+
/** 2 when the gate refuses this deploy (blocked on a gated tenant); every other verdict is 0. */
|
|
56
|
+
export declare function gateExitCode(answer?: null | PolicyGateAnswer): number;
|
|
57
|
+
/**
|
|
58
|
+
* The gate's verdict on stdout: a headline with its sentence, the release it judged, how many of its
|
|
59
|
+
* findings block, and the first of them. Every blocking finding of the release counts, whatever the
|
|
60
|
+
* tenant runs today.
|
|
61
|
+
*/
|
|
62
|
+
export declare function gateLines(answer: PolicyGateAnswer): string[];
|
|
63
|
+
/**
|
|
64
|
+
* The line a completed deploy prints about its gate. A blocked verdict on an ephemeral or sandbox
|
|
65
|
+
* tenant refused nothing (the deploy went ahead), so it says so rather than reading as a failure.
|
|
66
|
+
*/
|
|
67
|
+
export declare function deployedGateLine(answer: PolicyGateAnswer): string;
|
|
68
|
+
/**
|
|
69
|
+
* The verdict of a gate that measures a change against the live branch (set-live, publish): its
|
|
70
|
+
* status, what blocks, and the first blocking findings. `subject` is what changes: a branch set live,
|
|
71
|
+
* the branch a release was deployed as (`release deploy --set_live`), or an object saved to live.
|
|
72
|
+
*/
|
|
73
|
+
export declare function liveGateLines(answer: LiveGateAnswer, subject: 'branch' | 'release' | 'save'): string[];
|
|
74
|
+
/**
|
|
75
|
+
* A save the live-branch publish gate refused (`function create`, `function edit`): what goes on
|
|
76
|
+
* stdout (the verdict, or under `-o json` `{created|updated: false, message, policy_gate}`) and the
|
|
77
|
+
* error the command exits 2 with, saying nothing was saved and how to proceed (`rerun` is the same
|
|
78
|
+
* command with `--policy-override`).
|
|
79
|
+
*/
|
|
80
|
+
export declare function publishRefusal(refused: GateRefused, options: {
|
|
81
|
+
json: boolean;
|
|
82
|
+
outcome: 'created' | 'updated';
|
|
83
|
+
rerun: string;
|
|
84
|
+
}): {
|
|
85
|
+
lines: string[];
|
|
86
|
+
message: string;
|
|
87
|
+
};
|
|
88
|
+
/** The refusal of a `--policy-override` given without a reason, before any request is made. */
|
|
89
|
+
export declare const BLANK_POLICY_OVERRIDE = "--policy-override needs a reason: say why this change may proceed past the policy gate.";
|
|
90
|
+
/** True when `--policy-override` was given but holds only whitespace. */
|
|
91
|
+
export declare function blankPolicyOverride(reason?: string): boolean;
|
|
92
|
+
/** A value quoted for the shell when it needs it. */
|
|
93
|
+
export declare function quoted(value: string): string;
|
|
94
|
+
/**
|
|
95
|
+
* What to do about a refusal: override it (the credential may, and `command` is how), or ask for
|
|
96
|
+
* the permission. A refusal whose override was denied already says so in its message.
|
|
97
|
+
*/
|
|
98
|
+
export declare function gateOverrideHint(answer: PolicyGateAnswer, command: string): string;
|
|
99
|
+
/** A deploy the policy gate refused: the platform's sentence and the gate's answer. */
|
|
100
|
+
export interface GateRefused {
|
|
101
|
+
answer: PolicyGateAnswer;
|
|
102
|
+
message: string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* What a command the gate refused prints on stdout: the verdict, or under `-o json` the refusal as
|
|
106
|
+
* `{deployed: false, message, policy_gate}`. The command then exits 2 with the message.
|
|
107
|
+
*/
|
|
108
|
+
export declare function gateRefusalOutput(refused: GateRefused, json: boolean): string[];
|
|
109
|
+
/** A policy gate's refusal read from a failed response: its message and answer, or `null` for any other failure. */
|
|
110
|
+
export declare function gateRefusal(response: Response): Promise<GateRefused | null>;
|
|
111
|
+
/**
|
|
112
|
+
* A change refused because it weakens a policy active and mandatory on the branch, from a caller
|
|
113
|
+
* without the `workspace:policy` update permission (`policy_weakening_permission_required`). A
|
|
114
|
+
* release deploy or archive import with set live adds the branch it landed, which stays.
|
|
115
|
+
*/
|
|
116
|
+
export interface WeakeningRefused {
|
|
117
|
+
message: string;
|
|
118
|
+
/** `unavailable`: the policies could not be read to check it, so only the permission proceeds (`policies` is empty). */
|
|
119
|
+
payload: {
|
|
120
|
+
branch?: {
|
|
121
|
+
id?: number;
|
|
122
|
+
label?: string;
|
|
123
|
+
};
|
|
124
|
+
code: string;
|
|
125
|
+
gate?: string;
|
|
126
|
+
level?: string;
|
|
127
|
+
permission?: string;
|
|
128
|
+
policies?: string[];
|
|
129
|
+
unavailable?: boolean;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** The weakening refusal read from a failed response, or `null` for any other failure. */
|
|
133
|
+
export declare function weakeningRefusal(response: Response): Promise<null | WeakeningRefused>;
|
|
134
|
+
/** What a gate preview needs from the command that asks for it. */
|
|
135
|
+
export interface GateHost {
|
|
136
|
+
verboseFetch(url: string, options: RequestInit, verbose: boolean, authToken?: string): Promise<Response>;
|
|
137
|
+
}
|
|
138
|
+
/** A preview the platform could not answer: the HTTP status, its message and the refusal's payload. */
|
|
139
|
+
export interface GateUnavailable {
|
|
140
|
+
message: string;
|
|
141
|
+
payload?: unknown;
|
|
142
|
+
status: number;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* `GET workspace/{ws}/tenant/{tenant}/policy_gate?release_name=`: what the gate would decide for the
|
|
146
|
+
* release on the tenant, without deploying. A refusal (the Policies feature is off, the credential
|
|
147
|
+
* cannot read policies, no such tenant or release) is answered as `unavailable` rather than thrown.
|
|
148
|
+
*/
|
|
149
|
+
export declare function fetchPolicyGate(host: GateHost, target: {
|
|
150
|
+
profile: ProfileConfig;
|
|
151
|
+
release: string;
|
|
152
|
+
tenant: string;
|
|
153
|
+
verbose: boolean;
|
|
154
|
+
workspace: string;
|
|
155
|
+
}): Promise<{
|
|
156
|
+
answer: PolicyGateAnswer;
|
|
157
|
+
} | {
|
|
158
|
+
unavailable: GateUnavailable;
|
|
159
|
+
}>;
|
|
160
|
+
/** Why a preview is not available, in a few words for a line that must not fail its command. */
|
|
161
|
+
export declare function unavailableReason(unavailable: GateUnavailable): string;
|