@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,71 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import PolicyCommand from '../../../policy-command.js';
|
|
3
|
+
import { evaluationEvidence, notStoredLine, policyCheckWarning, policyExitCode, policySummary, } from '../../../utils/policy/feedback.js';
|
|
4
|
+
/** How many clear objects a trial names per rule; the rest are counted. */
|
|
5
|
+
const TRIAL_CLEAR_NAMES = 5;
|
|
6
|
+
/**
|
|
7
|
+
* What a trial says it is, then each rule's clear objects: the objects it examined and found
|
|
8
|
+
* nothing on. A trial is never stored, so its answer is all there is to show.
|
|
9
|
+
*/
|
|
10
|
+
export function trialLines(key, clear) {
|
|
11
|
+
const lines = [`Trial of ${key}: this policy alone, evaluated without storing a run. It is not a report and gates nothing.`];
|
|
12
|
+
if (!clear?.length)
|
|
13
|
+
return lines;
|
|
14
|
+
lines.push('Clear objects (examined, nothing found):');
|
|
15
|
+
for (const rule of clear) {
|
|
16
|
+
const names = (rule.items ?? []).map((item) => item.name ?? '').filter(Boolean).slice(0, TRIAL_CLEAR_NAMES);
|
|
17
|
+
const total = rule.total ?? 0;
|
|
18
|
+
const more = total > names.length ? `${names.length > 0 ? ', ' : ''}${total - names.length} more` : '';
|
|
19
|
+
lines.push(` ${rule.rule_id ?? ''} ${total}${names.length > 0 || more ? ` ${names.join(', ')}${more}` : ''}`);
|
|
20
|
+
}
|
|
21
|
+
return lines;
|
|
22
|
+
}
|
|
23
|
+
export default class PolicyEvaluate extends PolicyCommand {
|
|
24
|
+
static description = 'Evaluate active branch policies and report their findings; blocking findings exit 2. With --policy, try one policy alone, whatever its state, without storing a run';
|
|
25
|
+
static examples = ['$ xano policy evaluate -o json', '$ xano policy evaluate --run-detail', '$ xano policy evaluate --summary -o json', '$ xano policy evaluate --policy AUTH-001'];
|
|
26
|
+
static flags = {
|
|
27
|
+
...PolicyCommand.policyFlags,
|
|
28
|
+
policy: Flags.string({
|
|
29
|
+
description: 'Try this one policy (its key) alone, whatever its state: nothing is stored, and the answer lists each rule\'s clear objects',
|
|
30
|
+
}),
|
|
31
|
+
'run-detail': Flags.boolean({
|
|
32
|
+
default: false,
|
|
33
|
+
description: 'Also print what this run recorded: each policy description and the settings each rule ran with',
|
|
34
|
+
}),
|
|
35
|
+
summary: Flags.boolean({
|
|
36
|
+
default: false,
|
|
37
|
+
description: 'Answer the run summary and its first 50 findings instead of the whole run; `xano policy runs <id>` pages through the rest',
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
async run() {
|
|
41
|
+
const { flags } = await this.parse(PolicyEvaluate);
|
|
42
|
+
if (flags.policy !== undefined && !flags.policy.trim())
|
|
43
|
+
this.error('--policy must name a policy key.');
|
|
44
|
+
const { request } = this.policyTarget(flags);
|
|
45
|
+
// The evaluation answers with its run, so the run's own snapshot names any unnamed rule. The
|
|
46
|
+
// summary answer carries the same snapshot and results, and only the first findings.
|
|
47
|
+
const body = { ...(flags.summary ? { answer: 'summary' } : {}), ...(flags.policy ? { policy: flags.policy } : {}) };
|
|
48
|
+
const result = (await request('/evaluate', 'POST', Object.keys(body).length > 0 ? body : undefined));
|
|
49
|
+
if (flags.output === 'json')
|
|
50
|
+
this.log(JSON.stringify(result, null, 2));
|
|
51
|
+
else {
|
|
52
|
+
for (const line of policySummary(result.policy_check, evaluationEvidence(result)))
|
|
53
|
+
this.log(line);
|
|
54
|
+
// A trial is never stored by design; the "cannot record runs" note is about credentials.
|
|
55
|
+
const notStored = flags.policy ? null : notStoredLine(result);
|
|
56
|
+
if (notStored)
|
|
57
|
+
this.log(notStored);
|
|
58
|
+
if (flags.policy)
|
|
59
|
+
for (const line of trialLines(flags.policy, result.clear))
|
|
60
|
+
this.log(line);
|
|
61
|
+
if (flags['run-detail'])
|
|
62
|
+
this.logRunDetail(result);
|
|
63
|
+
}
|
|
64
|
+
const warning = policyCheckWarning(result.policy_check);
|
|
65
|
+
if (warning)
|
|
66
|
+
this.warn(warning);
|
|
67
|
+
const code = policyExitCode(result.policy_check);
|
|
68
|
+
if (code)
|
|
69
|
+
process.exitCode = code;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import PolicyCommand from '../../../policy-command.js';
|
|
2
|
+
export default class PolicyList extends PolicyCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import PolicyCommand from '../../../policy-command.js';
|
|
2
|
+
import { listItems } from '../../../utils/policy/request.js';
|
|
3
|
+
export default class PolicyList extends PolicyCommand {
|
|
4
|
+
static description = 'List workspace policies and their descriptions';
|
|
5
|
+
static examples = ['$ xano policy list -o json'];
|
|
6
|
+
static flags = { ...PolicyCommand.policyFlags };
|
|
7
|
+
async run() {
|
|
8
|
+
const { flags } = await this.parse(PolicyList);
|
|
9
|
+
const { request } = this.policyTarget(flags);
|
|
10
|
+
const result = await request();
|
|
11
|
+
if (flags.output === 'json') {
|
|
12
|
+
this.log(JSON.stringify(result, null, 2));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const policies = listItems(result);
|
|
16
|
+
if (policies.length === 0)
|
|
17
|
+
this.log('No policies found.');
|
|
18
|
+
for (const policy of policies)
|
|
19
|
+
this.log(`${policy.key} ${policy.active ? `Active, ${policy.enforcement === 'mandatory' ? 'Mandatory' : 'Advisory'}` : 'Inactive'} ${policy.title ?? ''} (ID: ${policy.id}, Version ${policy.version})`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import PolicyCommand from '../../../policy-command.js';
|
|
2
|
+
export default class PolicyParse extends PolicyCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
file: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
stdin: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import PolicyCommand from '../../../policy-command.js';
|
|
2
|
+
export default class PolicyParse extends PolicyCommand {
|
|
3
|
+
static args = { ...PolicyCommand.sourceArgs };
|
|
4
|
+
static description = 'Validate and format policy XanoScript using the platform parser, with rule warnings against the branch';
|
|
5
|
+
static examples = [
|
|
6
|
+
'$ xano policy parse policies/AUTH-001.xs',
|
|
7
|
+
'$ xano policy parse --file policies/AUTH-001.xs',
|
|
8
|
+
];
|
|
9
|
+
static flags = { ...PolicyCommand.policyFlags, ...PolicyCommand.sourceFlags };
|
|
10
|
+
async run() {
|
|
11
|
+
const { args, flags } = await this.parse(PolicyParse);
|
|
12
|
+
const { request } = this.policyTarget(flags);
|
|
13
|
+
const parsed = await this.parseSource(request, { source: this.readSource(flags, args.file) });
|
|
14
|
+
this.log(flags.output === 'json' ? JSON.stringify(parsed, null, 2) : parsed.source);
|
|
15
|
+
if (flags.output !== 'json')
|
|
16
|
+
this.logRuleWarnings(parsed);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import PolicyCommand from '../../../policy-command.js';
|
|
2
|
+
export default class PolicyPublish extends PolicyCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
file: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
message: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
stdin: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
};
|
|
19
|
+
run(): Promise<void>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import PolicyCommand from '../../../policy-command.js';
|
|
2
|
+
import { listAllPolicies, sameKey } from '../../../utils/policy/request.js';
|
|
3
|
+
export default class PolicyPublish extends PolicyCommand {
|
|
4
|
+
static args = { ...PolicyCommand.sourceArgs };
|
|
5
|
+
static description = 'Create or update a workspace policy from native XanoScript';
|
|
6
|
+
static examples = [
|
|
7
|
+
'$ xano policy publish policies/AUTH-001.xs',
|
|
8
|
+
'$ xano policy publish --file policies/AUTH-001.xs',
|
|
9
|
+
'$ xano policy publish --file policies/AUTH-001.xs -m "Tightened the scope"',
|
|
10
|
+
];
|
|
11
|
+
static flags = { ...PolicyCommand.policyFlags, ...PolicyCommand.sourceFlags, ...PolicyCommand.publishFlags };
|
|
12
|
+
async run() {
|
|
13
|
+
const { args, flags } = await this.parse(PolicyPublish);
|
|
14
|
+
const target = this.policyTarget(flags);
|
|
15
|
+
const { request } = target;
|
|
16
|
+
const source = this.readSource(flags, args.file);
|
|
17
|
+
// The key picks PUT or POST, so the list is read alongside the parse rather than after it.
|
|
18
|
+
const [parsed, policies] = await Promise.all([this.parseSource(request, { source }), listAllPolicies(request)]);
|
|
19
|
+
// A key that differs only in case names the same policy, which PUT updates, key included.
|
|
20
|
+
const existing = policies.find((policy) => sameKey(policy.key, parsed.policy.key));
|
|
21
|
+
const body = { data: { source: parsed.source } };
|
|
22
|
+
if (flags.message)
|
|
23
|
+
body.message = flags.message;
|
|
24
|
+
const saved = (await request(existing ? `/${existing.id}` : '', existing ? 'PUT' : 'POST', body));
|
|
25
|
+
if (typeof saved?.id !== 'number') {
|
|
26
|
+
this.error('The platform did not return the saved policy. Check `xano policy list` before retrying.');
|
|
27
|
+
}
|
|
28
|
+
this.logRuleWarnings(parsed);
|
|
29
|
+
if (flags.output === 'json') {
|
|
30
|
+
this.log(JSON.stringify(saved, null, 2));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// A save identical to the stored definition writes nothing: no version, history entry or audit record.
|
|
34
|
+
this.log(saved.unchanged === true
|
|
35
|
+
? `No changes to ${parsed.policy.key} (Version ${saved.version}) in ${this.where(target)}.`
|
|
36
|
+
: `Published ${parsed.policy.key} (Version ${saved.version}) to ${this.where(target)}.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Interfaces } from '@oclif/core';
|
|
2
|
+
import PolicyCommand from '../../../policy-command.js';
|
|
3
|
+
export default class PolicyRuns extends PolicyCommand {
|
|
4
|
+
static args: {
|
|
5
|
+
run_id: Interfaces.Arg<string | undefined, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static examples: string[];
|
|
9
|
+
static flags: {
|
|
10
|
+
advisory: Interfaces.BooleanFlag<boolean>;
|
|
11
|
+
all: Interfaces.BooleanFlag<boolean>;
|
|
12
|
+
blocking: Interfaces.BooleanFlag<boolean>;
|
|
13
|
+
kind: Interfaces.OptionFlag<string[] | undefined, Interfaces.CustomOptions>;
|
|
14
|
+
limit: Interfaces.OptionFlag<number | undefined, Interfaces.CustomOptions>;
|
|
15
|
+
object: Interfaces.OptionFlag<string[] | undefined, Interfaces.CustomOptions>;
|
|
16
|
+
offset: Interfaces.OptionFlag<number | undefined, Interfaces.CustomOptions>;
|
|
17
|
+
policy: Interfaces.OptionFlag<string[] | undefined, Interfaces.CustomOptions>;
|
|
18
|
+
recheck: Interfaces.BooleanFlag<boolean>;
|
|
19
|
+
release: Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
|
|
20
|
+
'release-id': Interfaces.OptionFlag<number | undefined, Interfaces.CustomOptions>;
|
|
21
|
+
rule: Interfaces.OptionFlag<string[] | undefined, Interfaces.CustomOptions>;
|
|
22
|
+
'run-detail': Interfaces.BooleanFlag<boolean>;
|
|
23
|
+
search: Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
|
|
24
|
+
severity: Interfaces.OptionFlag<string[] | undefined, Interfaces.CustomOptions>;
|
|
25
|
+
tag: Interfaces.OptionFlag<string[] | undefined, Interfaces.CustomOptions>;
|
|
26
|
+
branch: Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
|
|
27
|
+
output: Interfaces.OptionFlag<string, Interfaces.CustomOptions>;
|
|
28
|
+
workspace: Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
|
|
29
|
+
config: Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
|
|
30
|
+
profile: Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
|
|
31
|
+
verbose: Interfaces.BooleanFlag<boolean>;
|
|
32
|
+
};
|
|
33
|
+
run(): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* `--all`: every finding that matches, read a page of 500 at a time in the platform's order, as
|
|
36
|
+
* one page. A run of many pages says on stderr what it is reading.
|
|
37
|
+
*/
|
|
38
|
+
private everyFinding;
|
|
39
|
+
/** The retained runs, newest first; the flags that read one run's findings are refused. */
|
|
40
|
+
private listRuns;
|
|
41
|
+
/**
|
|
42
|
+
* `--release <name>`: the release's stored check (`GET release/{id}/policy_run`) and a page of its
|
|
43
|
+
* findings, as a run is read. `--recheck` checks the release again from its archive first. A
|
|
44
|
+
* release with no check says so; under `-o json` it is `{run: null}`.
|
|
45
|
+
*/
|
|
46
|
+
private readRelease;
|
|
47
|
+
/** The release's ID from its name, reading the workspace's releases a page at a time until it is found. */
|
|
48
|
+
private releaseId;
|
|
49
|
+
/**
|
|
50
|
+
* One run's summary and a page of its findings (`path`), or under `-o json` the summary with
|
|
51
|
+
* `findings` and `findings_page`, plus what `extra` adds to it.
|
|
52
|
+
*/
|
|
53
|
+
private reportRun;
|
|
54
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import PolicyCommand from '../../../policy-command.js';
|
|
3
|
+
import { quoted } from '../../../utils/policy/gate.js';
|
|
4
|
+
import { releasePolicyRunLines, releaseRunLines } from '../../../utils/policy/release.js';
|
|
5
|
+
import { listItems } from '../../../utils/policy/request.js';
|
|
6
|
+
import { policyRunSummary, policyRunTable } from '../../../utils/policy/runs.js';
|
|
7
|
+
/** A branch retains this many runs, and the run list serves at most this many. */
|
|
8
|
+
const RUNS_RETAINED = 20;
|
|
9
|
+
/** How many findings a run read prints unless `--limit` says otherwise, and the most a page holds. */
|
|
10
|
+
const FINDINGS_LIMIT = 100;
|
|
11
|
+
const FINDINGS_MAX = 500;
|
|
12
|
+
/** The findings route's list filters, each a repeatable flag of the same name. */
|
|
13
|
+
const LIST_FILTERS = ['policy', 'rule', 'severity', 'kind', 'object', 'tag'];
|
|
14
|
+
/** The flags that page or filter one run's findings, so a run list refuses them. */
|
|
15
|
+
const FINDING_FLAGS = ['offset', 'all', 'advisory', 'blocking', ...LIST_FILTERS, 'search'];
|
|
16
|
+
/** How many releases a lookup by name reads a request, and the most requests it makes. */
|
|
17
|
+
const RELEASES_PAGE = 100;
|
|
18
|
+
const RELEASE_PAGES = 50;
|
|
19
|
+
export default class PolicyRuns extends PolicyCommand {
|
|
20
|
+
static args = {
|
|
21
|
+
run_id: Args.string({
|
|
22
|
+
description: 'Run ID to read; omit to list the retained runs, newest first',
|
|
23
|
+
required: false,
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
static description = "List the stored policy check runs, or read one of them a page of findings at a time. --release reads a release's check instead: the run stored when the release was cut, evaluated on what it ships against the policies it ships";
|
|
27
|
+
static examples = [
|
|
28
|
+
'$ xano policy runs',
|
|
29
|
+
'$ xano policy runs --limit 5',
|
|
30
|
+
'$ xano policy runs 1674 --run-detail',
|
|
31
|
+
'$ xano policy runs 1674 --offset 100 --limit 100',
|
|
32
|
+
'$ xano policy runs 1674 --all -o json',
|
|
33
|
+
'$ xano policy runs 1674 --blocking --policy AUTH-001 --severity critical --severity high',
|
|
34
|
+
'$ xano policy runs 1674 -o json',
|
|
35
|
+
'$ xano policy runs --release v1.2',
|
|
36
|
+
'$ xano policy runs --release v1.2 --blocking --all -o json',
|
|
37
|
+
'$ xano policy runs --release v1.2 --recheck',
|
|
38
|
+
'$ xano policy runs --release-id 12',
|
|
39
|
+
];
|
|
40
|
+
static flags = {
|
|
41
|
+
...PolicyCommand.policyFlags,
|
|
42
|
+
advisory: Flags.boolean({
|
|
43
|
+
default: false,
|
|
44
|
+
description: 'With a run ID, only the advisory findings',
|
|
45
|
+
exclusive: ['blocking'],
|
|
46
|
+
}),
|
|
47
|
+
all: Flags.boolean({
|
|
48
|
+
default: false,
|
|
49
|
+
description: 'With a run ID, read every page and print every finding that matches, instead of one page',
|
|
50
|
+
exclusive: ['offset', 'limit'],
|
|
51
|
+
}),
|
|
52
|
+
blocking: Flags.boolean({
|
|
53
|
+
default: false,
|
|
54
|
+
description: "With a run ID, only the blocking findings (an active mandatory policy's)",
|
|
55
|
+
exclusive: ['advisory'],
|
|
56
|
+
}),
|
|
57
|
+
kind: Flags.string({
|
|
58
|
+
description: 'With a run ID, only findings on this object kind (query, function, table, ...); repeatable',
|
|
59
|
+
multiple: true,
|
|
60
|
+
}),
|
|
61
|
+
limit: Flags.integer({
|
|
62
|
+
description: `How many runs to list, newest first (1-${RUNS_RETAINED}, default ${RUNS_RETAINED}: a branch retains at most ${RUNS_RETAINED}); with a run ID, how many findings to print (1-${FINDINGS_MAX}, default ${FINDINGS_LIMIT})`,
|
|
63
|
+
min: 1,
|
|
64
|
+
}),
|
|
65
|
+
object: Flags.string({
|
|
66
|
+
description: 'With a run ID, only findings on this object, written type:id as a finding names it (query:18); repeatable',
|
|
67
|
+
multiple: true,
|
|
68
|
+
}),
|
|
69
|
+
offset: Flags.integer({
|
|
70
|
+
description: "With a run ID, how many findings to skip, in the platform's order (blocking first)",
|
|
71
|
+
min: 0,
|
|
72
|
+
}),
|
|
73
|
+
policy: Flags.string({
|
|
74
|
+
description: 'With a run ID, only findings of this policy key; repeatable',
|
|
75
|
+
multiple: true,
|
|
76
|
+
}),
|
|
77
|
+
recheck: Flags.boolean({
|
|
78
|
+
default: false,
|
|
79
|
+
description: "With --release, check the release again from its archive and store the new run, then read it (a read-only credential is refused)",
|
|
80
|
+
}),
|
|
81
|
+
release: Flags.string({
|
|
82
|
+
description: "Read this release's policy check (by release name) instead of a branch's run; the finding flags page and filter it as they do a run's",
|
|
83
|
+
exclusive: ['branch', 'release-id'],
|
|
84
|
+
}),
|
|
85
|
+
'release-id': Flags.integer({
|
|
86
|
+
description: 'Read a release policy check by release ID, without looking up its name',
|
|
87
|
+
exclusive: ['branch', 'release'],
|
|
88
|
+
min: 1,
|
|
89
|
+
}),
|
|
90
|
+
rule: Flags.string({
|
|
91
|
+
description: 'With a run ID, only findings of this rule ID (AUTH-001.R1); repeatable',
|
|
92
|
+
multiple: true,
|
|
93
|
+
}),
|
|
94
|
+
'run-detail': Flags.boolean({
|
|
95
|
+
default: false,
|
|
96
|
+
description: 'With a run ID, also print the policy descriptions and rule settings that run recorded',
|
|
97
|
+
}),
|
|
98
|
+
search: Flags.string({
|
|
99
|
+
description: 'With a run ID, only findings whose policy key or title, rule title, object name or message contains this text',
|
|
100
|
+
}),
|
|
101
|
+
severity: Flags.string({
|
|
102
|
+
description: 'With a run ID, only findings of this severity (critical, high, medium, low); repeatable',
|
|
103
|
+
multiple: true,
|
|
104
|
+
}),
|
|
105
|
+
tag: Flags.string({
|
|
106
|
+
description: 'With a run ID, only findings whose policy or object carries this tag; repeatable',
|
|
107
|
+
multiple: true,
|
|
108
|
+
}),
|
|
109
|
+
};
|
|
110
|
+
async run() {
|
|
111
|
+
const { args, flags } = await this.parse(PolicyRuns);
|
|
112
|
+
if (flags.release !== undefined || flags['release-id'] !== undefined)
|
|
113
|
+
return this.readRelease(flags, args.run_id);
|
|
114
|
+
if (flags.recheck)
|
|
115
|
+
this.error('--recheck checks a release again: `xano policy runs --release <name> --recheck`.');
|
|
116
|
+
const { request } = this.policyTarget(flags);
|
|
117
|
+
const wanted = args.run_id?.trim() ?? '';
|
|
118
|
+
if (wanted === '')
|
|
119
|
+
return this.listRuns(request, flags);
|
|
120
|
+
// Runs older than the one `policy status` reads are reachable here by id.
|
|
121
|
+
if (!/^\d+$/.test(wanted))
|
|
122
|
+
this.error(`"${wanted}" is not a run ID. Run \`xano policy runs\` for the retained runs.`);
|
|
123
|
+
const limit = findingsLimit(this, flags);
|
|
124
|
+
// The run's summary and its findings a page at a time: neither reads the whole run, which can hold tens of thousands.
|
|
125
|
+
const head = (await request(`/run/${wanted}/summary`));
|
|
126
|
+
await this.reportRun({ flags, head, limit, path: `/run/${wanted}/findings`, reading: `run ${wanted}`, request });
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* `--all`: every finding that matches, read a page of 500 at a time in the platform's order, as
|
|
130
|
+
* one page. A run of many pages says on stderr what it is reading.
|
|
131
|
+
*/
|
|
132
|
+
async everyFinding(request, path, reading, query) {
|
|
133
|
+
const items = [];
|
|
134
|
+
let total = Number.POSITIVE_INFINITY;
|
|
135
|
+
while (items.length < total) {
|
|
136
|
+
// eslint-disable-next-line no-await-in-loop -- one page at a time, each starting where the last ended
|
|
137
|
+
const page = (await request(path, 'GET', undefined, { ...query, limit: String(FINDINGS_MAX), offset: String(items.length) }));
|
|
138
|
+
if (items.length === 0 && page.total > FINDINGS_MAX)
|
|
139
|
+
this.logToStderr(`Reading ${page.total} findings of ${reading}, ${FINDINGS_MAX} a request (${Math.ceil(page.total / FINDINGS_MAX)} requests)...`);
|
|
140
|
+
total = page.total ?? 0;
|
|
141
|
+
if ((page.items ?? []).length === 0)
|
|
142
|
+
break;
|
|
143
|
+
items.push(...page.items);
|
|
144
|
+
}
|
|
145
|
+
return { items, limit: items.length, offset: 0, total: Number.isFinite(total) ? total : items.length };
|
|
146
|
+
}
|
|
147
|
+
/** The retained runs, newest first; the flags that read one run's findings are refused. */
|
|
148
|
+
async listRuns(request, flags) {
|
|
149
|
+
if (flags['run-detail'])
|
|
150
|
+
this.error('--run-detail reads one run: `xano policy runs <id> --run-detail`.');
|
|
151
|
+
const paging = FINDING_FLAGS.find(name => flags[name] !== undefined && flags[name] !== false);
|
|
152
|
+
if (paging)
|
|
153
|
+
this.error(`--${paging} pages one run's findings: \`xano policy runs <id> --${paging} ...\`.`);
|
|
154
|
+
const limit = flags.limit ?? RUNS_RETAINED;
|
|
155
|
+
if (limit > RUNS_RETAINED)
|
|
156
|
+
this.error(`--limit lists at most ${RUNS_RETAINED} runs: a branch retains only its newest ${RUNS_RETAINED}.`);
|
|
157
|
+
const result = await request('/run', 'GET', undefined, { limit: String(limit) });
|
|
158
|
+
if (flags.output === 'json') {
|
|
159
|
+
this.log(JSON.stringify(result, null, 2));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const runs = listItems(result);
|
|
163
|
+
if (runs.length === 0) {
|
|
164
|
+
this.log('No policy runs retained on this branch.');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
for (const line of policyRunTable(runs))
|
|
168
|
+
this.log(line);
|
|
169
|
+
this.log('Only the most recent runs are retained per branch. Read one with `xano policy runs <id> --run-detail`.');
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* `--release <name>`: the release's stored check (`GET release/{id}/policy_run`) and a page of its
|
|
173
|
+
* findings, as a run is read. `--recheck` checks the release again from its archive first. A
|
|
174
|
+
* release with no check says so; under `-o json` it is `{run: null}`.
|
|
175
|
+
*/
|
|
176
|
+
async readRelease(flags, runId) {
|
|
177
|
+
if (runId?.trim())
|
|
178
|
+
this.error('Read one run by its ID, or a release\'s check with --release, not both.');
|
|
179
|
+
const name = flags.release?.trim() ?? String(flags['release-id'] ?? '');
|
|
180
|
+
if (!name)
|
|
181
|
+
this.error('--release needs a release name.');
|
|
182
|
+
const limit = findingsLimit(this, flags);
|
|
183
|
+
const { request } = this.policyTarget(flags, { label: 'Release policy check', path: '' });
|
|
184
|
+
const releaseId = flags['release-id'] ?? await this.releaseId(request, name);
|
|
185
|
+
const route = `/release/${releaseId}/policy_run`;
|
|
186
|
+
const answer = (flags.recheck ? await request(route, 'POST', {}) : await request(route));
|
|
187
|
+
const check = flags.recheck ? { check: answer.check ?? null } : {};
|
|
188
|
+
// The recheck's outcome in a line: recorded with its verdict, or why nothing was recorded.
|
|
189
|
+
const rechecked = flags.recheck ? releasePolicyRunLines(answer.check, name).slice(0, 1).map(line => `Rechecked. ${line.trim()}`) : [];
|
|
190
|
+
const head = answer.run ?? null;
|
|
191
|
+
if (!head) {
|
|
192
|
+
if (flags.output === 'json') {
|
|
193
|
+
this.log(JSON.stringify({ ...check, run: null }, null, 2));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
for (const line of rechecked)
|
|
197
|
+
this.log(line);
|
|
198
|
+
this.log(`Release ${name} has no policy check.${flags.recheck ? '' : ' `--recheck` checks it now from its archive.'}`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
await this.reportRun({
|
|
202
|
+
check,
|
|
203
|
+
flags,
|
|
204
|
+
head,
|
|
205
|
+
limit,
|
|
206
|
+
next: flags['release-id'] ? `xano policy runs --release-id ${releaseId}` : `xano policy runs --release ${quoted(name)}`,
|
|
207
|
+
path: `${route}/findings`,
|
|
208
|
+
preface: [...rechecked, ...releaseRunLines(name, releaseId, head)],
|
|
209
|
+
reading: `release ${name}`,
|
|
210
|
+
request,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/** The release's ID from its name, reading the workspace's releases a page at a time until it is found. */
|
|
214
|
+
async releaseId(request, name) {
|
|
215
|
+
for (let page = 1; page <= RELEASE_PAGES; page++) {
|
|
216
|
+
// eslint-disable-next-line no-await-in-loop -- pages in order, stopping at the release
|
|
217
|
+
const data = await request('/release', 'GET', undefined, { page: String(page), per_page: String(RELEASES_PAGE) });
|
|
218
|
+
const releases = Array.isArray(data) ? data : listItems(data);
|
|
219
|
+
const match = releases.find(release => release.name === name);
|
|
220
|
+
if (match)
|
|
221
|
+
return match.id;
|
|
222
|
+
if (releases.length < RELEASES_PAGE)
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
return this.error(`Release '${name}' not found. \`xano release list\` lists the workspace's releases.`);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* One run's summary and a page of its findings (`path`), or under `-o json` the summary with
|
|
229
|
+
* `findings` and `findings_page`, plus what `extra` adds to it.
|
|
230
|
+
*/
|
|
231
|
+
async reportRun(opts) {
|
|
232
|
+
const { flags, head, limit, path, request } = opts;
|
|
233
|
+
const query = findingsQuery(flags, limit);
|
|
234
|
+
const page = flags.all
|
|
235
|
+
? await this.everyFinding(request, path, opts.reading, query)
|
|
236
|
+
: (await request(path, 'GET', undefined, query));
|
|
237
|
+
const items = page.items ?? [];
|
|
238
|
+
const offset = page.offset ?? 0;
|
|
239
|
+
const total = page.total ?? items.length;
|
|
240
|
+
if (flags.output === 'json') {
|
|
241
|
+
const next = offset + items.length < total && items.length > 0 ? offset + items.length : null;
|
|
242
|
+
const findingsPage = { limit: page.limit ?? limit, next_offset: next, offset, total };
|
|
243
|
+
this.log(JSON.stringify({ ...head, ...opts.check, findings: items, findings_page: findingsPage }, null, 2));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
for (const line of opts.preface ?? [])
|
|
247
|
+
this.log(line);
|
|
248
|
+
const filtered = Object.keys(query).some(name => !['limit', 'offset'].includes(name));
|
|
249
|
+
for (const line of policyRunSummary({ ...head, findings: items }, { filtered, next: opts.next, offset, total }))
|
|
250
|
+
this.log(line);
|
|
251
|
+
if (flags['run-detail'])
|
|
252
|
+
this.logRunDetail(head);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** How many findings a page prints: `--limit`, at most 500. */
|
|
256
|
+
function findingsLimit(command, flags) {
|
|
257
|
+
const limit = flags.limit ?? FINDINGS_LIMIT;
|
|
258
|
+
if (limit > FINDINGS_MAX)
|
|
259
|
+
command.error(`--limit prints at most ${FINDINGS_MAX} findings a page; page on with --offset.`);
|
|
260
|
+
return limit;
|
|
261
|
+
}
|
|
262
|
+
/** The findings route's query for one page: `blocking` true or false, and each list filter given. */
|
|
263
|
+
function findingsQuery(flags, limit) {
|
|
264
|
+
const query = { limit: String(limit), offset: String(flags.offset ?? 0) };
|
|
265
|
+
if (flags.blocking || flags.advisory)
|
|
266
|
+
query.blocking = String(flags.blocking);
|
|
267
|
+
for (const name of LIST_FILTERS)
|
|
268
|
+
if (flags[name]?.length)
|
|
269
|
+
query[name] = flags[name];
|
|
270
|
+
if (flags.search?.trim())
|
|
271
|
+
query.q = flags.search;
|
|
272
|
+
return query;
|
|
273
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import PolicyCommand from '../../../policy-command.js';
|
|
2
|
+
export default class PolicyStatus extends PolicyCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
'fail-on-findings': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
'run-detail': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
};
|
|
15
|
+
run(): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* The run every policy's `latest_run` answer was decided against, or none when the branch has no
|
|
18
|
+
* run: its summary, which counts each policy's findings without listing them.
|
|
19
|
+
*/
|
|
20
|
+
private latestRun;
|
|
21
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import PolicyCommand from '../../../policy-command.js';
|
|
3
|
+
import { policyResultSummary } from '../../../utils/policy/findings.js';
|
|
4
|
+
import { listAllPolicies } from '../../../utils/policy/request.js';
|
|
5
|
+
import { computeStatusRows, enforcementLabel, findingsLabel, statusExitCode, statusExitReason, statusLabel, } from '../../../utils/policy/status.js';
|
|
6
|
+
export default class PolicyStatus extends PolicyCommand {
|
|
7
|
+
static description = 'Show the latest policy check results without evaluating';
|
|
8
|
+
static examples = ['$ xano policy status -o json', '$ xano policy status --fail-on-findings -o json', '$ xano policy status --run-detail'];
|
|
9
|
+
static flags = {
|
|
10
|
+
...PolicyCommand.policyFlags,
|
|
11
|
+
'fail-on-findings': Flags.boolean({
|
|
12
|
+
default: false,
|
|
13
|
+
description: 'Exit 2 for current blocking findings, whatever else is true; otherwise exit 1 for stale, missing or errored evaluation evidence',
|
|
14
|
+
}),
|
|
15
|
+
'run-detail': Flags.boolean({
|
|
16
|
+
default: false,
|
|
17
|
+
description: 'Also print what the latest run recorded: each policy description and the settings each rule ran with',
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
async run() {
|
|
21
|
+
const { flags } = await this.parse(PolicyStatus);
|
|
22
|
+
const { request } = this.policyTarget(flags);
|
|
23
|
+
const policies = await listAllPolicies(request);
|
|
24
|
+
const run = await this.latestRun(request, policies);
|
|
25
|
+
const rows = computeStatusRows(policies, run);
|
|
26
|
+
const failOnFindings = flags['fail-on-findings'] ? { exit: statusExitCode(rows), reason: statusExitReason(rows) } : undefined;
|
|
27
|
+
if (flags.output === 'json') {
|
|
28
|
+
this.log(JSON.stringify({ policies, run: run ?? null, status: rows, ...(failOnFindings ? { fail_on_findings: failOnFindings } : {}) }, null, 2));
|
|
29
|
+
}
|
|
30
|
+
else if (rows.length === 0)
|
|
31
|
+
this.log('No policies found.');
|
|
32
|
+
else {
|
|
33
|
+
for (const row of rows)
|
|
34
|
+
this.log(`${row.key} ${statusLabel(row)} ${enforcementLabel(row.enforcement)} ${findingsLabel(row)} ${row.title ?? ''}`);
|
|
35
|
+
const counted = new Set(rows.filter((row) => row.counted).map((row) => row.key));
|
|
36
|
+
for (const line of policyResultSummary(run?.results?.filter((result) => counted.has(result.policy_key ?? ''))))
|
|
37
|
+
this.log(line);
|
|
38
|
+
if (flags['run-detail'] && run)
|
|
39
|
+
this.logRunDetail(run);
|
|
40
|
+
if (failOnFindings?.reason)
|
|
41
|
+
this.log(failOnFindings.reason);
|
|
42
|
+
}
|
|
43
|
+
if (failOnFindings?.exit)
|
|
44
|
+
process.exitCode = failOnFindings.exit;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The run every policy's `latest_run` answer was decided against, or none when the branch has no
|
|
48
|
+
* run: its summary, which counts each policy's findings without listing them.
|
|
49
|
+
*/
|
|
50
|
+
async latestRun(request, policies) {
|
|
51
|
+
const runId = policies.map((policy) => policy.latest_run?.run_id).find(Boolean);
|
|
52
|
+
return runId ? (await request(`/run/${runId}/summary`)) : undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { releasePolicyRunLines } from '../../../utils/policy/release.js';
|
|
3
4
|
export default class ReleaseCreate extends BaseCommand {
|
|
4
5
|
static args = {
|
|
5
6
|
name: Args.string({
|
|
@@ -11,6 +12,10 @@ export default class ReleaseCreate extends BaseCommand {
|
|
|
11
12
|
static examples = [
|
|
12
13
|
`$ xano release create "v1.0" --branch main
|
|
13
14
|
Created release: v1.0 - ID: 10
|
|
15
|
+
Branch: main
|
|
16
|
+
Policy check: fail (run 1712), 2 blocking, 1 advisory findings
|
|
17
|
+
Findings: xano policy runs --release v1.0
|
|
18
|
+
Its blocking findings block a deploy to a standard or run tenant: xano tenant deploy_release <tenant> --release v1.0 --check
|
|
14
19
|
`,
|
|
15
20
|
`$ xano release create "v1.1-hotfix" --branch main --hotfix --description "Critical fix" -o json`,
|
|
16
21
|
];
|
|
@@ -90,6 +95,8 @@ Created release: v1.0 - ID: 10
|
|
|
90
95
|
this.log(` Branch: ${release.branch}`);
|
|
91
96
|
if (release.hotfix)
|
|
92
97
|
this.log(` Hotfix: true`);
|
|
98
|
+
for (const line of releasePolicyRunLines(release.policy_run, release.name ?? args.name))
|
|
99
|
+
this.log(line);
|
|
93
100
|
}
|
|
94
101
|
}
|
|
95
102
|
catch (error) {
|