@cantinasecurity/apex-cli 0.1.17 → 0.1.18
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/.claude/skills/apex-cli/SKILL.md +30 -11
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.claude.json +1 -1
- package/.mcp.codex.json +1 -1
- package/MARKETPLACE.md +1 -1
- package/README.md +102 -5
- package/dist/apex.js +15 -1
- package/dist/api-client.js +255 -35
- package/dist/args.js +8 -1
- package/dist/auth.js +171 -54
- package/dist/claim-proof.js +65 -0
- package/dist/commands.js +253 -19
- package/dist/config.js +357 -23
- package/dist/findings.js +50 -9
- package/dist/help.js +14 -1
- package/dist/local-source-scan.js +200 -24
- package/dist/mcp.js +128 -19
- package/dist/onboarding.js +661 -0
- package/dist/redaction.js +34 -0
- package/dist/session.js +10 -1
- package/dist/shell.js +10 -3
- package/dist/telemetry.js +83 -12
- package/dist/workspace-binding.js +16 -6
- package/package.json +3 -1
- package/skills/apex-cli/SKILL.md +43 -11
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const CLAIM_DEADLINE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/;
|
|
2
|
+
function isLeapYear(year) {
|
|
3
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
4
|
+
}
|
|
5
|
+
function daysInMonth(year, month) {
|
|
6
|
+
if (month === 2)
|
|
7
|
+
return isLeapYear(year) ? 29 : 28;
|
|
8
|
+
return [4, 6, 9, 11].includes(month) ? 30 : 31;
|
|
9
|
+
}
|
|
10
|
+
/** Parses only the RFC3339 shape emitted by the onboarding API. */
|
|
11
|
+
export function parseClaimDeadlineMs(value) {
|
|
12
|
+
if (typeof value !== "string")
|
|
13
|
+
return null;
|
|
14
|
+
const match = CLAIM_DEADLINE_PATTERN.exec(value);
|
|
15
|
+
if (!match)
|
|
16
|
+
return null;
|
|
17
|
+
const year = Number(match[1]);
|
|
18
|
+
const month = Number(match[2]);
|
|
19
|
+
const day = Number(match[3]);
|
|
20
|
+
const hour = Number(match[4]);
|
|
21
|
+
const minute = Number(match[5]);
|
|
22
|
+
const second = Number(match[6]);
|
|
23
|
+
const offset = match[7] ?? "";
|
|
24
|
+
if (year < 1 ||
|
|
25
|
+
month < 1 ||
|
|
26
|
+
month > 12 ||
|
|
27
|
+
day < 1 ||
|
|
28
|
+
day > daysInMonth(year, month) ||
|
|
29
|
+
hour > 23 ||
|
|
30
|
+
minute > 59 ||
|
|
31
|
+
second > 59) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (offset !== "Z") {
|
|
35
|
+
const offsetHour = Number(offset.slice(1, 3));
|
|
36
|
+
const offsetMinute = Number(offset.slice(4, 6));
|
|
37
|
+
if (offsetHour > 23 || offsetMinute > 59)
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const parsed = Date.parse(value);
|
|
41
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Classifies the locally stored claim proof without mutating it.
|
|
45
|
+
*
|
|
46
|
+
* A missing or malformed deadline is treated conservatively as available: an
|
|
47
|
+
* older CLI or interrupted write may have left a valid proof without usable
|
|
48
|
+
* deadline metadata, and only the server can safely declare that proof dead.
|
|
49
|
+
* A known deadline is exclusive, so the proof expires exactly at that instant.
|
|
50
|
+
*/
|
|
51
|
+
export function getClaimProofState(credentials, nowMs = Date.now()) {
|
|
52
|
+
if (!credentials?.claimToken?.trim())
|
|
53
|
+
return "absent";
|
|
54
|
+
const rawDeadline = credentials.claimExpiresAt;
|
|
55
|
+
const deadline = typeof rawDeadline === "string" ? rawDeadline.trim() : "";
|
|
56
|
+
if (!deadline)
|
|
57
|
+
return "available";
|
|
58
|
+
const deadlineMs = parseClaimDeadlineMs(deadline);
|
|
59
|
+
if (deadlineMs === null || !Number.isFinite(nowMs))
|
|
60
|
+
return "available";
|
|
61
|
+
return deadlineMs > nowMs ? "available" : "expired";
|
|
62
|
+
}
|
|
63
|
+
export function isClaimProofAvailable(credentials, nowMs = Date.now()) {
|
|
64
|
+
return getClaimProofState(credentials, nowMs) === "available";
|
|
65
|
+
}
|
package/dist/commands.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { canPromptInteractively, getFlagList, getFlagString, isJsonMode, } from "./args.js";
|
|
2
|
-
import { getMe, logout } from "./auth.js";
|
|
3
|
-
import { ApiError } from "./api-client.js";
|
|
2
|
+
import { getMe, login, logout } from "./auth.js";
|
|
3
|
+
import { ApiError, formatApiError, getApiErrorCode, } from "./api-client.js";
|
|
4
4
|
import { openInBrowser } from "./browser.js";
|
|
5
|
-
import {
|
|
5
|
+
import { getClaimProofState, isClaimProofAvailable, parseClaimDeadlineMs, } from "./claim-proof.js";
|
|
6
|
+
import { loadConfig, updateConfig } from "./config.js";
|
|
6
7
|
import { createFindingComment, fetchScanExport, fetchScanFindings, isFindingUuid, normalizeFindingRef, resolveFindingSelection, resolveScanSelection, startFindingFixReviewScan, submitFindingFeedback, } from "./findings.js";
|
|
7
8
|
import { prepareExplicitScanSources, supportsLegacyRemoteFlow, } from "./local-source-scan.js";
|
|
8
9
|
import { logLine, printJson, withLoadingIndicator } from "./output.js";
|
|
10
|
+
import { claimOnboardingSession, isProofOnlyOnboardingCredentials, redeemOnboardingCode, revokeGuestOnboardingSession, } from "./onboarding.js";
|
|
9
11
|
import { confirm } from "./prompt.js";
|
|
10
12
|
import { cancelScan, fetchScanProgress, fetchWorkspaceScans, findMostRelevantActiveScan, getScanDisplayLabel, getScanDisplayId, getTrackedScanId, isActiveScanStatus, matchesScanId, selectDefaultScanToCancel, } from "./scan.js";
|
|
11
13
|
import { createServiceApiKey, isServiceApiScope, listServiceApiKeys, revokeServiceApiKey, SERVICE_API_SCOPES, } from "./service-keys.js";
|
|
@@ -13,7 +15,7 @@ import { chooseCompany, createWorkspaceBinding, ensureAuthenticated, remediateMi
|
|
|
13
15
|
import { createWorkspaceBindingFromSummary, fetchCompanyCredits, fetchCompanyWorkspaces, findWorkspaceByRef, } from "./workspaces.js";
|
|
14
16
|
import { loadWorkspaceBinding, saveWorkspaceBinding } from "./workspace-binding.js";
|
|
15
17
|
import { commandSetup as runCliSetup } from "./setup.js";
|
|
16
|
-
import { getTelemetryStatus, setTelemetryEnabled, } from "./telemetry.js";
|
|
18
|
+
import { getTelemetryStatus, setTelemetryAuthKind, setTelemetryEnabled, setTelemetryOnboardingOutcome, } from "./telemetry.js";
|
|
17
19
|
import { commandUpdate as runCliUpdate } from "./update.js";
|
|
18
20
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
19
21
|
import path from "node:path";
|
|
@@ -54,13 +56,94 @@ const SERVICE_KEY_NAME_SCHEMA = z
|
|
|
54
56
|
.min(1)
|
|
55
57
|
.max(100)
|
|
56
58
|
.refine((value) => !SERVICE_KEY_NAME_CONTROL_CHARACTER_PATTERN.test(value));
|
|
59
|
+
function getRedeemTelemetryOutcome(error) {
|
|
60
|
+
switch (getApiErrorCode(error)) {
|
|
61
|
+
case "INVALID_CODE_FORMAT":
|
|
62
|
+
case "CODE_NOT_FOUND":
|
|
63
|
+
return "invalid";
|
|
64
|
+
case "CODE_EXPIRED":
|
|
65
|
+
case "CODE_REVOKED":
|
|
66
|
+
case "CODE_PAUSED":
|
|
67
|
+
return "expired";
|
|
68
|
+
case "CODE_EXHAUSTED":
|
|
69
|
+
return "exhausted";
|
|
70
|
+
case "RATE_LIMITED":
|
|
71
|
+
return "rate_limited";
|
|
72
|
+
case "TRIAL_CAPACITY":
|
|
73
|
+
return "capacity";
|
|
74
|
+
case "ACTIVE_GUEST_SESSION":
|
|
75
|
+
return "active_guest_session";
|
|
76
|
+
default:
|
|
77
|
+
return error instanceof Error && /trial code (?:format is invalid|is required)/i.test(error.message)
|
|
78
|
+
? "invalid"
|
|
79
|
+
: "error";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function getClaimTelemetryOutcome(error) {
|
|
83
|
+
switch (getApiErrorCode(error)) {
|
|
84
|
+
case "CLAIM_INVALID":
|
|
85
|
+
return "invalid";
|
|
86
|
+
case "CLAIM_EXPIRED":
|
|
87
|
+
return "claim_expired";
|
|
88
|
+
case "ALREADY_CLAIMED":
|
|
89
|
+
return "already_claimed";
|
|
90
|
+
case "SCAN_IN_PROGRESS":
|
|
91
|
+
return "scan_in_progress";
|
|
92
|
+
case "EMAIL_UNVERIFIED":
|
|
93
|
+
return "email_unverified";
|
|
94
|
+
case "FINGERPRINT_MISMATCH":
|
|
95
|
+
return "fingerprint_mismatch";
|
|
96
|
+
default:
|
|
97
|
+
return "error";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function getCommandAuthState(client, me) {
|
|
101
|
+
let credentials = null;
|
|
102
|
+
try {
|
|
103
|
+
credentials = await client.getCredentials();
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Local metadata is only for UX and telemetry; it must not break scan commands.
|
|
107
|
+
}
|
|
108
|
+
const guest = credentials?.guest === true || me.user.role?.trim().toLowerCase() === "guest";
|
|
109
|
+
return {
|
|
110
|
+
authKind: guest ? "guest" : "user",
|
|
111
|
+
// Ordinary device login intentionally keeps an unclaimed trial proof. Keep
|
|
112
|
+
// reminding the user after that auth-kind transition until claim consumes it.
|
|
113
|
+
guestCredentials: credentials &&
|
|
114
|
+
(credentials.guest === true || isClaimProofAvailable(credentials))
|
|
115
|
+
? credentials
|
|
116
|
+
: null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function logGuestScanFooter(authState, flags) {
|
|
120
|
+
if (isJsonMode(flags) || !authState.guestCredentials)
|
|
121
|
+
return;
|
|
122
|
+
const claimState = getClaimProofState(authState.guestCredentials);
|
|
123
|
+
if (claimState === "expired") {
|
|
124
|
+
logLine("Guest trial claim window expired — run 'apex login' to continue with a Cantina account.", flags);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const deadlineTime = parseClaimDeadlineMs(authState.guestCredentials.claimExpiresAt);
|
|
128
|
+
const deadline = deadlineTime !== null
|
|
129
|
+
? new Date(deadlineTime).toISOString()
|
|
130
|
+
: "the claim deadline";
|
|
131
|
+
if (claimState === "absent") {
|
|
132
|
+
logLine(`Guest trial claim proof is missing — run 'apex redeem <code>' again on this machine before ${deadline}, then run 'apex claim'.`, flags);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const message = authState.authKind === "guest"
|
|
136
|
+
? `Guest scan — create a Cantina account and run 'apex claim' by ${deadline} to keep it.`
|
|
137
|
+
: `Trial results are still unclaimed — run 'apex claim' by ${deadline} to keep them.`;
|
|
138
|
+
logLine(message, flags);
|
|
139
|
+
}
|
|
140
|
+
export function getRedeemExitCode(result) {
|
|
141
|
+
return result.redeemed ? 0 : 2;
|
|
142
|
+
}
|
|
57
143
|
async function saveCompanyDefault(companyId) {
|
|
58
144
|
const config = await loadConfig();
|
|
59
145
|
if (config.defaultCompanyId !== companyId) {
|
|
60
|
-
await
|
|
61
|
-
...config,
|
|
62
|
-
defaultCompanyId: companyId,
|
|
63
|
-
});
|
|
146
|
+
await updateConfig({ defaultCompanyId: companyId });
|
|
64
147
|
}
|
|
65
148
|
}
|
|
66
149
|
function formatCommandArgument(value) {
|
|
@@ -564,7 +647,13 @@ export async function initializeInteractiveSession(client, cwd, flags) {
|
|
|
564
647
|
});
|
|
565
648
|
}
|
|
566
649
|
export async function commandLogin(client, flags) {
|
|
567
|
-
|
|
650
|
+
// Explicit login is the escape hatch from an unavailable guest session. It
|
|
651
|
+
// intentionally bypasses ordinary-command claim steering while preserving
|
|
652
|
+
// the proof for an authoritative `apex claim` attempt.
|
|
653
|
+
const me = await login(client, {
|
|
654
|
+
noOpen: flags["no-open"] === true,
|
|
655
|
+
quiet: isJsonMode(flags),
|
|
656
|
+
});
|
|
568
657
|
if (isJsonMode(flags)) {
|
|
569
658
|
printJson(me);
|
|
570
659
|
return me;
|
|
@@ -572,6 +661,75 @@ export async function commandLogin(client, flags) {
|
|
|
572
661
|
logLine(`Authenticated as ${me.user.username ?? me.user.id}`, flags);
|
|
573
662
|
return me;
|
|
574
663
|
}
|
|
664
|
+
export async function commandRedeem(client, flags, code) {
|
|
665
|
+
try {
|
|
666
|
+
if (flags.replace === true) {
|
|
667
|
+
const warning = "Warning: --replace revokes the current guest trial and queues its unclaimed source, scans, and findings for deletion before redeeming the new code.";
|
|
668
|
+
if (isJsonMode(flags)) {
|
|
669
|
+
process.stderr.write(`${warning}\n`);
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
logLine(warning, flags);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
const result = await redeemOnboardingCode(client, {
|
|
676
|
+
code,
|
|
677
|
+
replace: flags.replace === true,
|
|
678
|
+
});
|
|
679
|
+
setTelemetryOnboardingOutcome(result.outcome);
|
|
680
|
+
if (isJsonMode(flags)) {
|
|
681
|
+
printJson(result);
|
|
682
|
+
return result;
|
|
683
|
+
}
|
|
684
|
+
if (!result.redeemed) {
|
|
685
|
+
logLine(`Trial redemption skipped: ${result.warning}.`, flags);
|
|
686
|
+
return result;
|
|
687
|
+
}
|
|
688
|
+
logLine(`Trial ${result.outcome}: ${result.grant.scans} standard scan${result.grant.scans === 1 ? "" : "s"}.`, flags);
|
|
689
|
+
logLine(`Guest session expires: ${result.guestSessionExpiresAt}`, flags);
|
|
690
|
+
logLine(`Claim deadline: ${result.claimDeadline}`, flags);
|
|
691
|
+
logLine(result.notice, flags);
|
|
692
|
+
logLine(`Terms and retention: ${result.termsUrl}`, flags);
|
|
693
|
+
return result;
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
setTelemetryOnboardingOutcome(getRedeemTelemetryOutcome(error));
|
|
697
|
+
throw error;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
export async function commandClaim(client, cwd, flags) {
|
|
701
|
+
const json = isJsonMode(flags);
|
|
702
|
+
try {
|
|
703
|
+
const result = await claimOnboardingSession(client, cwd, {
|
|
704
|
+
noOpen: json || flags["no-open"] === true,
|
|
705
|
+
quiet: json,
|
|
706
|
+
...(json
|
|
707
|
+
? {
|
|
708
|
+
onVerification: async (login) => {
|
|
709
|
+
process.stderr.write(`Complete Cantina account setup with code ${escapeTerminalControlCharacters(login.userCode)} at ${escapeTerminalControlCharacters(login.verificationUrl)}\n`);
|
|
710
|
+
},
|
|
711
|
+
}
|
|
712
|
+
: {}),
|
|
713
|
+
});
|
|
714
|
+
setTelemetryOnboardingOutcome("claimed");
|
|
715
|
+
if (json) {
|
|
716
|
+
printJson(result);
|
|
717
|
+
return result;
|
|
718
|
+
}
|
|
719
|
+
logLine(`Claimed into: ${result.company.name ?? result.company.handle ?? result.company.id}`, flags);
|
|
720
|
+
if (result.scanUrl) {
|
|
721
|
+
logLine(`View: ${result.scanUrl}`, flags);
|
|
722
|
+
}
|
|
723
|
+
if (result.discountCode) {
|
|
724
|
+
logLine(`First credit-pack discount: ${result.discountCode}`, flags);
|
|
725
|
+
}
|
|
726
|
+
return result;
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
setTelemetryOnboardingOutcome(getClaimTelemetryOutcome(error));
|
|
730
|
+
throw error;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
575
733
|
export async function commandCredits(client, cwd, flags) {
|
|
576
734
|
const me = await ensureAuthenticated(client, flags);
|
|
577
735
|
const binding = await loadWorkspaceBinding(cwd);
|
|
@@ -607,10 +765,49 @@ export async function commandCredits(client, cwd, flags) {
|
|
|
607
765
|
return payload;
|
|
608
766
|
}
|
|
609
767
|
export async function commandLogout(client, flags) {
|
|
610
|
-
await
|
|
768
|
+
const credentials = await client.getCredentials().catch(() => null);
|
|
769
|
+
const proofOnly = isProofOnlyOnboardingCredentials(credentials);
|
|
770
|
+
const hasGuestLifecycle = credentials?.guest === true || proofOnly;
|
|
771
|
+
if (hasGuestLifecycle && flags.force !== true) {
|
|
772
|
+
if (!canPromptForConfirmation(flags)) {
|
|
773
|
+
throw new Error("Logging out revokes this guest trial and queues its unclaimed data for deletion. Re-run with --force to confirm.");
|
|
774
|
+
}
|
|
775
|
+
const approved = await confirm("Logging out revokes this guest trial and queues all unclaimed source, scans, and findings for deletion. Continue?", false);
|
|
776
|
+
if (!approved) {
|
|
777
|
+
logLine("Guest logout cancelled; the trial and claim proof were kept.", flags);
|
|
778
|
+
return {
|
|
779
|
+
loggedOut: false,
|
|
780
|
+
guestTrialRevoked: false,
|
|
781
|
+
confirmationRequired: true,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const guestTrialRevoked = hasGuestLifecycle
|
|
786
|
+
? await revokeGuestOnboardingSession(client, {
|
|
787
|
+
clearLocalOnTerminal: true,
|
|
788
|
+
expectedCredentials: credentials ?? undefined,
|
|
789
|
+
})
|
|
790
|
+
: false;
|
|
791
|
+
if (!hasGuestLifecycle) {
|
|
792
|
+
await logout(client, credentials);
|
|
793
|
+
}
|
|
794
|
+
const serverRevocationUnavailable = hasGuestLifecycle && !guestTrialRevoked;
|
|
611
795
|
if (!isJsonMode(flags)) {
|
|
612
|
-
logLine(
|
|
796
|
+
logLine(guestTrialRevoked
|
|
797
|
+
? "Guest trial revoked; unclaimed trial data is queued for deletion."
|
|
798
|
+
: serverRevocationUnavailable
|
|
799
|
+
? proofOnly
|
|
800
|
+
? "Guest trial proof cleared locally, but server revocation could not be confirmed."
|
|
801
|
+
: "Guest access cleared locally, but the terminal server session could not be revoked."
|
|
802
|
+
: "Logged out", flags);
|
|
613
803
|
}
|
|
804
|
+
return {
|
|
805
|
+
loggedOut: true,
|
|
806
|
+
guestTrialRevoked,
|
|
807
|
+
...(serverRevocationUnavailable
|
|
808
|
+
? { serverRevocationUnavailable: true }
|
|
809
|
+
: {}),
|
|
810
|
+
};
|
|
614
811
|
}
|
|
615
812
|
export async function commandServiceKeyCreate(client, flags) {
|
|
616
813
|
const name = normalizeServiceKeyName(getFlagString(flags, "name"));
|
|
@@ -774,7 +971,7 @@ export async function commandDoctor(client, cwd, flags) {
|
|
|
774
971
|
if (isJsonMode(flags)) {
|
|
775
972
|
const payload = {
|
|
776
973
|
ok: false,
|
|
777
|
-
error:
|
|
974
|
+
error: formatApiError(error),
|
|
778
975
|
};
|
|
779
976
|
printJson(payload);
|
|
780
977
|
if (error instanceof Error) {
|
|
@@ -786,7 +983,9 @@ export async function commandDoctor(client, cwd, flags) {
|
|
|
786
983
|
}
|
|
787
984
|
}
|
|
788
985
|
export async function commandStatus(client, cwd, flags) {
|
|
789
|
-
await ensureAuthenticated(client, flags);
|
|
986
|
+
const me = await ensureAuthenticated(client, flags);
|
|
987
|
+
const authState = await getCommandAuthState(client, me);
|
|
988
|
+
setTelemetryAuthKind(authState.authKind);
|
|
790
989
|
const binding = await requireWorkspaceBinding(cwd);
|
|
791
990
|
const requestedScanId = getFlagString(flags, "scan");
|
|
792
991
|
const scanId = requestedScanId ?? binding.lastScanId;
|
|
@@ -807,6 +1006,7 @@ export async function commandStatus(client, cwd, flags) {
|
|
|
807
1006
|
if (binding.lastScanUrl && (!requestedScanId || requestedScanId === binding.lastScanId)) {
|
|
808
1007
|
logLine(`View: ${binding.lastScanUrl}`, flags);
|
|
809
1008
|
}
|
|
1009
|
+
logGuestScanFooter(authState, flags);
|
|
810
1010
|
return { binding, scanId, progress };
|
|
811
1011
|
}
|
|
812
1012
|
export async function commandConnect(client, cwd, flags, provider) {
|
|
@@ -838,6 +1038,8 @@ export async function commandScan(client, cwd, flags) {
|
|
|
838
1038
|
throw new Error("PR scans require at least one --pr <number> selection.");
|
|
839
1039
|
}
|
|
840
1040
|
const me = await ensureAuthenticated(client, flags);
|
|
1041
|
+
const authState = await getCommandAuthState(client, me);
|
|
1042
|
+
setTelemetryAuthKind(authState.authKind);
|
|
841
1043
|
const selection = await selectWorkspaceTarget(cwd, me, flags);
|
|
842
1044
|
const result = await withLoadingIndicator("Resolving workspace and scan plan...", flags, () => resolveWorkspaceSelection(client, selection));
|
|
843
1045
|
if (!result.resolve.workspaceId) {
|
|
@@ -920,14 +1122,19 @@ export async function commandScan(client, cwd, flags) {
|
|
|
920
1122
|
});
|
|
921
1123
|
});
|
|
922
1124
|
}
|
|
1125
|
+
const trackedScanId = getTrackedScanId(scan);
|
|
923
1126
|
const binding = {
|
|
924
1127
|
...createWorkspaceBinding(result, result.binding),
|
|
925
1128
|
workspaceId,
|
|
926
|
-
lastScanId:
|
|
1129
|
+
lastScanId: trackedScanId,
|
|
927
1130
|
lastScanUrl: scan.scanUrl ?? null,
|
|
928
1131
|
};
|
|
929
|
-
await
|
|
930
|
-
|
|
1132
|
+
const [workspaceBindingWrite, companyDefaultWrite] = await Promise.allSettled([
|
|
1133
|
+
Promise.resolve().then(() => saveWorkspaceBinding(cwd, binding)),
|
|
1134
|
+
Promise.resolve().then(() => saveCompanyDefault(result.company.id)),
|
|
1135
|
+
]);
|
|
1136
|
+
const workspaceBindingSaved = workspaceBindingWrite.status === "fulfilled";
|
|
1137
|
+
const companyDefaultSaved = companyDefaultWrite.status === "fulfilled";
|
|
931
1138
|
const summarySources = prScanSource
|
|
932
1139
|
? getPrScanSelectedSources(result.sources, prScanSource)
|
|
933
1140
|
: result.sources;
|
|
@@ -946,6 +1153,14 @@ export async function commandScan(client, cwd, flags) {
|
|
|
946
1153
|
apexScanId: scan.apexScanId ?? scan.bedrockScanId ?? null,
|
|
947
1154
|
},
|
|
948
1155
|
binding,
|
|
1156
|
+
...(!workspaceBindingSaved || !companyDefaultSaved
|
|
1157
|
+
? {
|
|
1158
|
+
localPersistence: {
|
|
1159
|
+
workspaceBindingSaved,
|
|
1160
|
+
companyDefaultSaved,
|
|
1161
|
+
},
|
|
1162
|
+
}
|
|
1163
|
+
: {}),
|
|
949
1164
|
};
|
|
950
1165
|
if (isJsonMode(flags)) {
|
|
951
1166
|
printJson(payload);
|
|
@@ -954,16 +1169,31 @@ export async function commandScan(client, cwd, flags) {
|
|
|
954
1169
|
logLine(`Authenticated as ${me.user.username ?? me.user.id}`, flags);
|
|
955
1170
|
logLine(`Company: ${result.company.handle ?? result.company.id}`, flags);
|
|
956
1171
|
logLine(`Workspace: ${result.workspaceName}`, flags);
|
|
1172
|
+
if (!workspaceBindingSaved) {
|
|
1173
|
+
logLine(`Warning: the scan started, but Apex could not save this directory's workspace binding. Keep scan ID ${trackedScanId ?? "from the result"}; repair local repository permissions and bind this directory again before using status or findings commands here.`, flags);
|
|
1174
|
+
}
|
|
1175
|
+
if (!companyDefaultSaved) {
|
|
1176
|
+
logLine("Warning: the scan started, but Apex could not save the default company. Pass --company explicitly until local Apex config permissions are repaired.", flags);
|
|
1177
|
+
}
|
|
957
1178
|
logLine(`Sources selected: ${summarySources.length}`, flags);
|
|
958
1179
|
logLine(`Plan: ${summaryPlannedSources.length - localSnapshotCount} remote, ${localSnapshotCount} local snapshot`, flags);
|
|
959
|
-
logLine(`Scan: ${scan.status}${
|
|
1180
|
+
logLine(`Scan: ${scan.status}${trackedScanId ? ` (${trackedScanId})` : ""}`, flags);
|
|
960
1181
|
if (scan.scanUrl) {
|
|
961
1182
|
logLine(`View: ${scan.scanUrl}`, flags);
|
|
962
1183
|
}
|
|
963
1184
|
if (scan.fullScanRecommendation) {
|
|
964
1185
|
logLine(`Recommendation: ${scan.fullScanRecommendation}`, flags);
|
|
965
1186
|
}
|
|
966
|
-
|
|
1187
|
+
if (workspaceBindingSaved) {
|
|
1188
|
+
logLine("Next: run `apex status` to watch progress, then `apex findings` when the scan finishes.", flags);
|
|
1189
|
+
}
|
|
1190
|
+
else if (trackedScanId) {
|
|
1191
|
+
logLine(`Next: repair local repository permissions, run \`apex workspace use ${workspaceId}\`, then use \`apex status --scan ${trackedScanId}\` and \`apex findings --scan ${trackedScanId}\`.`, flags);
|
|
1192
|
+
}
|
|
1193
|
+
else {
|
|
1194
|
+
logLine(`Next: repair local repository permissions, then run \`apex workspace use ${workspaceId}\` before using status or findings.`, flags);
|
|
1195
|
+
}
|
|
1196
|
+
logGuestScanFooter(authState, flags);
|
|
967
1197
|
return payload;
|
|
968
1198
|
}
|
|
969
1199
|
export async function commandWorkspaces(client, cwd, flags) {
|
|
@@ -1161,7 +1391,9 @@ export async function commandCancelScan(client, cwd, flags, requestedScanId) {
|
|
|
1161
1391
|
return payload;
|
|
1162
1392
|
}
|
|
1163
1393
|
export async function commandFindings(client, cwd, flags) {
|
|
1164
|
-
await ensureAuthenticated(client, flags);
|
|
1394
|
+
const me = await ensureAuthenticated(client, flags);
|
|
1395
|
+
const authState = await getCommandAuthState(client, me);
|
|
1396
|
+
setTelemetryAuthKind(authState.authKind);
|
|
1165
1397
|
const binding = await requireWorkspaceBinding(cwd);
|
|
1166
1398
|
const { scan, response } = await withLoadingIndicator("Loading findings...", flags, async () => {
|
|
1167
1399
|
const scan = await resolveScanSelection({
|
|
@@ -1190,6 +1422,7 @@ export async function commandFindings(client, cwd, flags) {
|
|
|
1190
1422
|
logLine(`Scan: ${response.scan.displayName} (${response.scan.status})`, flags);
|
|
1191
1423
|
if (response.findings.length === 0) {
|
|
1192
1424
|
logLine("Findings: none for this scan.", flags);
|
|
1425
|
+
logGuestScanFooter(authState, flags);
|
|
1193
1426
|
return payload;
|
|
1194
1427
|
}
|
|
1195
1428
|
for (const finding of response.findings) {
|
|
@@ -1199,6 +1432,7 @@ export async function commandFindings(client, cwd, flags) {
|
|
|
1199
1432
|
logLine(`View: ${finding.findingUrl}`, flags);
|
|
1200
1433
|
}
|
|
1201
1434
|
}
|
|
1435
|
+
logGuestScanFooter(authState, flags);
|
|
1202
1436
|
return payload;
|
|
1203
1437
|
}
|
|
1204
1438
|
export async function commandFindingComment(client, cwd, flags, requestedFindingRef, content, parentCommentId) {
|