quoxcli 0.0.0-stage → 0.2.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/LICENSE +35 -0
- package/README.md +142 -2
- package/bin/quox.js +292 -0
- package/bin/quoxon.js +8 -0
- package/lib/api/endpoints/admin.js +125 -0
- package/lib/api/endpoints/aee.js +38 -0
- package/lib/api/endpoints/agent-attach.js +126 -0
- package/lib/api/endpoints/agents.js +56 -0
- package/lib/api/endpoints/assistants.js +50 -0
- package/lib/api/endpoints/audit.js +14 -0
- package/lib/api/endpoints/auth.js +50 -0
- package/lib/api/endpoints/awareness.js +34 -0
- package/lib/api/endpoints/backup-schedules.js +20 -0
- package/lib/api/endpoints/backups.js +86 -0
- package/lib/api/endpoints/brain.js +140 -0
- package/lib/api/endpoints/budgets.js +62 -0
- package/lib/api/endpoints/calendarq.js +71 -0
- package/lib/api/endpoints/catalog.js +14 -0
- package/lib/api/endpoints/chat.js +54 -0
- package/lib/api/endpoints/consent.js +26 -0
- package/lib/api/endpoints/contactsq.js +66 -0
- package/lib/api/endpoints/conversations.js +32 -0
- package/lib/api/endpoints/custom-roles.js +92 -0
- package/lib/api/endpoints/divisions.js +56 -0
- package/lib/api/endpoints/domains.js +38 -0
- package/lib/api/endpoints/engagement.js +231 -0
- package/lib/api/endpoints/engine.js +11 -0
- package/lib/api/endpoints/entity.js +50 -0
- package/lib/api/endpoints/executions.js +32 -0
- package/lib/api/endpoints/file.js +20 -0
- package/lib/api/endpoints/files.js +26 -0
- package/lib/api/endpoints/github.js +21 -0
- package/lib/api/endpoints/governance.js +43 -0
- package/lib/api/endpoints/hat.js +36 -0
- package/lib/api/endpoints/inbox.js +32 -0
- package/lib/api/endpoints/inboxq.js +120 -0
- package/lib/api/endpoints/index.js +228 -0
- package/lib/api/endpoints/invites.js +19 -0
- package/lib/api/endpoints/llm-policy.js +43 -0
- package/lib/api/endpoints/lucid.js +89 -0
- package/lib/api/endpoints/members.js +32 -0
- package/lib/api/endpoints/memory.js +75 -0
- package/lib/api/endpoints/mfa.js +32 -0
- package/lib/api/endpoints/mon.js +178 -0
- package/lib/api/endpoints/notesq.js +43 -0
- package/lib/api/endpoints/objectives.js +62 -0
- package/lib/api/endpoints/orgs.js +38 -0
- package/lib/api/endpoints/plugins.js +134 -0
- package/lib/api/endpoints/powers.js +75 -0
- package/lib/api/endpoints/prompts.js +69 -0
- package/lib/api/endpoints/qlarity.js +122 -0
- package/lib/api/endpoints/quoxbox.js +55 -0
- package/lib/api/endpoints/quoxchat.js +54 -0
- package/lib/api/endpoints/quoxflow.js +32 -0
- package/lib/api/endpoints/quoxplan.js +114 -0
- package/lib/api/endpoints/quoxseo.js +108 -0
- package/lib/api/endpoints/reach.js +47 -0
- package/lib/api/endpoints/runs.js +38 -0
- package/lib/api/endpoints/safety.js +46 -0
- package/lib/api/endpoints/scim.js +46 -0
- package/lib/api/endpoints/scopes.js +14 -0
- package/lib/api/endpoints/security.js +69 -0
- package/lib/api/endpoints/setup.js +90 -0
- package/lib/api/endpoints/skills.js +180 -0
- package/lib/api/endpoints/sso.js +67 -0
- package/lib/api/endpoints/tags.js +32 -0
- package/lib/api/endpoints/teams.js +56 -0
- package/lib/api/endpoints/telephony.js +46 -0
- package/lib/api/endpoints/tg.js +56 -0
- package/lib/api/endpoints/tools.js +50 -0
- package/lib/api/endpoints/traces.js +157 -0
- package/lib/api/endpoints/triggers.js +70 -0
- package/lib/api/endpoints/user-library.js +41 -0
- package/lib/api/endpoints/users.js +20 -0
- package/lib/api/endpoints/v1.js +499 -0
- package/lib/api/endpoints/vault.js +192 -0
- package/lib/api/endpoints/voice.js +22 -0
- package/lib/api/endpoints/volt.js +84 -0
- package/lib/api/endpoints/ward.js +40 -0
- package/lib/api/endpoints/webhooks.js +46 -0
- package/lib/api/endpoints/workflows.js +162 -0
- package/lib/api/endpoints/x.js +24 -0
- package/lib/api/endpoints.js +15 -0
- package/lib/commands/admin.js +359 -0
- package/lib/commands/aee.js +99 -0
- package/lib/commands/agent.js +201 -0
- package/lib/commands/agentAttach.js +1440 -0
- package/lib/commands/approvals.js +47 -0
- package/lib/commands/assistant.js +114 -0
- package/lib/commands/auth.js +458 -0
- package/lib/commands/awareness.js +88 -0
- package/lib/commands/backup.js +373 -0
- package/lib/commands/brain.js +994 -0
- package/lib/commands/budget.js +241 -0
- package/lib/commands/build.js +156 -0
- package/lib/commands/calendar.js +216 -0
- package/lib/commands/capabilities.js +34 -0
- package/lib/commands/chat.js +240 -0
- package/lib/commands/claude.js +354 -0
- package/lib/commands/config-cmd.js +106 -0
- package/lib/commands/consent.js +84 -0
- package/lib/commands/contacts.js +967 -0
- package/lib/commands/contactsSelftest.js +132 -0
- package/lib/commands/conversation.js +74 -0
- package/lib/commands/department.js +75 -0
- package/lib/commands/dev.js +620 -0
- package/lib/commands/devResident.js +314 -0
- package/lib/commands/devSupervisor.js +1050 -0
- package/lib/commands/discord.js +96 -0
- package/lib/commands/doctor.js +181 -0
- package/lib/commands/domain.js +114 -0
- package/lib/commands/done.js +277 -0
- package/lib/commands/engagement.js +586 -0
- package/lib/commands/entity.js +119 -0
- package/lib/commands/facebook.js +212 -0
- package/lib/commands/file.js +128 -0
- package/lib/commands/fleet.js +882 -0
- package/lib/commands/governance.js +638 -0
- package/lib/commands/gripe.js +324 -0
- package/lib/commands/hat.js +205 -0
- package/lib/commands/helpers.js +79 -0
- package/lib/commands/inbox.js +122 -0
- package/lib/commands/instagram.js +264 -0
- package/lib/commands/integration.js +100 -0
- package/lib/commands/inventory.js +115 -0
- package/lib/commands/invites.js +45 -0
- package/lib/commands/licence.js +280 -0
- package/lib/commands/linkedin.js +178 -0
- package/lib/commands/llm-policy.js +117 -0
- package/lib/commands/llm.js +45 -0
- package/lib/commands/lucid.js +591 -0
- package/lib/commands/mail.js +531 -0
- package/lib/commands/memory.js +284 -0
- package/lib/commands/mon.js +514 -0
- package/lib/commands/monitoring.js +86 -0
- package/lib/commands/motion.js +22 -0
- package/lib/commands/notes.js +1246 -0
- package/lib/commands/notesBatch.js +160 -0
- package/lib/commands/notesBridges.js +56 -0
- package/lib/commands/notesLinks.js +60 -0
- package/lib/commands/notification.js +57 -0
- package/lib/commands/nucleus.js +92 -0
- package/lib/commands/objective.js +229 -0
- package/lib/commands/onboard.js +328 -0
- package/lib/commands/open.js +56 -0
- package/lib/commands/org.js +266 -0
- package/lib/commands/plan.js +906 -0
- package/lib/commands/plugin-doctor.js +1091 -0
- package/lib/commands/plugin.js +539 -0
- package/lib/commands/policy.js +78 -0
- package/lib/commands/powers.js +296 -0
- package/lib/commands/prompt.js +350 -0
- package/lib/commands/qlarity.js +690 -0
- package/lib/commands/quoxbox.js +270 -0
- package/lib/commands/quoxchat.js +266 -0
- package/lib/commands/quoxseo.js +340 -0
- package/lib/commands/reach.js +158 -0
- package/lib/commands/role.js +286 -0
- package/lib/commands/run.js +132 -0
- package/lib/commands/safety.js +107 -0
- package/lib/commands/scim.js +88 -0
- package/lib/commands/scope.js +58 -0
- package/lib/commands/security.js +157 -0
- package/lib/commands/service.js +89 -0
- package/lib/commands/settings.js +99 -0
- package/lib/commands/setup.js +543 -0
- package/lib/commands/skillManifest.js +113 -0
- package/lib/commands/skillRightsFormat.js +38 -0
- package/lib/commands/skills.js +644 -0
- package/lib/commands/slot.js +1198 -0
- package/lib/commands/sso.js +229 -0
- package/lib/commands/status.js +137 -0
- package/lib/commands/tag.js +91 -0
- package/lib/commands/team.js +140 -0
- package/lib/commands/tg.js +240 -0
- package/lib/commands/tiktok.js +218 -0
- package/lib/commands/tool.js +216 -0
- package/lib/commands/trace.js +525 -0
- package/lib/commands/trigger.js +128 -0
- package/lib/commands/vault.js +1424 -0
- package/lib/commands/voice.js +182 -0
- package/lib/commands/volt.js +528 -0
- package/lib/commands/watch.js +115 -0
- package/lib/commands/webhook.js +137 -0
- package/lib/commands/workflow.js +503 -0
- package/lib/commands/x.js +439 -0
- package/lib/core/actingAs.js +136 -0
- package/lib/core/aee.js +53 -0
- package/lib/core/agentAlias.js +163 -0
- package/lib/core/approvalContext.js +61 -0
- package/lib/core/auth.js +350 -0
- package/lib/core/banner.js +143 -0
- package/lib/core/brainPrime.js +90 -0
- package/lib/core/callerSource.js +129 -0
- package/lib/core/claudeLs.js +94 -0
- package/lib/core/client.js +584 -0
- package/lib/core/completions.js +163 -0
- package/lib/core/config.js +551 -0
- package/lib/core/confirm.js +37 -0
- package/lib/core/deeplinks.js +76 -0
- package/lib/core/doneEngineFetch.js +219 -0
- package/lib/core/errors.js +132 -0
- package/lib/core/global-flags.js +90 -0
- package/lib/core/governedSession.js +402 -0
- package/lib/core/lazyCommands.js +119 -0
- package/lib/core/liveSessionsReport.js +112 -0
- package/lib/core/org-override.js +52 -0
- package/lib/core/output.js +239 -0
- package/lib/core/prodEnv.js +7 -0
- package/lib/core/quoxglyph.js +158 -0
- package/lib/core/residentAlarm.js +282 -0
- package/lib/core/residentAuth.js +243 -0
- package/lib/core/residentPump.js +229 -0
- package/lib/core/residentSeed.js +104 -0
- package/lib/core/sessionHeartbeat.js +192 -0
- package/lib/core/sessionsOverview.js +160 -0
- package/lib/core/slotKeeper.js +282 -0
- package/lib/core/supervisorEngine.js +864 -0
- package/lib/core/systemdInstaller.js +256 -0
- package/lib/core/version.js +87 -0
- package/lib/core/websiteSession.js +148 -0
- package/lib/lucid/LucidIntro.jsx +105 -0
- package/lib/lucid/blind.js +71 -0
- package/lib/lucid/deck.js +32 -0
- package/lib/lucid/fmt.js +139 -0
- package/lib/lucid/sitrep.js +173 -0
- package/lib/plugins/loader.js +93 -0
- package/lib/quoxon/activity.js +104 -0
- package/lib/quoxon/api.js +473 -0
- package/lib/quoxon/app.js +1262 -0
- package/lib/quoxon/capabilities.js +75 -0
- package/lib/quoxon/commands.js +154 -0
- package/lib/quoxon/connect.js +467 -0
- package/lib/quoxon/connectCard.js +28 -0
- package/lib/quoxon/context.js +68 -0
- package/lib/quoxon/copy.js +33 -0
- package/lib/quoxon/demo.fixtures.js +24 -0
- package/lib/quoxon/demo.js +35 -0
- package/lib/quoxon/egg.js +36 -0
- package/lib/quoxon/events.js +458 -0
- package/lib/quoxon/gallery.js +223 -0
- package/lib/quoxon/indicator.js +193 -0
- package/lib/quoxon/intent.js +41 -0
- package/lib/quoxon/keys.js +121 -0
- package/lib/quoxon/main.js +244 -0
- package/lib/quoxon/markdown.js +317 -0
- package/lib/quoxon/model.js +205 -0
- package/lib/quoxon/motion/clock.js +69 -0
- package/lib/quoxon/motion/color.js +35 -0
- package/lib/quoxon/motion/frame.js +14 -0
- package/lib/quoxon/motion/reveal.js +54 -0
- package/lib/quoxon/motion/ring.js +79 -0
- package/lib/quoxon/objects/agent.js +61 -0
- package/lib/quoxon/objects/crystal.js +100 -0
- package/lib/quoxon/objects/fetch.js +170 -0
- package/lib/quoxon/objects/index.js +85 -0
- package/lib/quoxon/objects/ladder.js +71 -0
- package/lib/quoxon/objects/memory.js +44 -0
- package/lib/quoxon/objects/model.js +118 -0
- package/lib/quoxon/objects/proof.js +110 -0
- package/lib/quoxon/objects/skill.js +105 -0
- package/lib/quoxon/objects/view.js +76 -0
- package/lib/quoxon/objects/workflow.js +119 -0
- package/lib/quoxon/observers.js +42 -0
- package/lib/quoxon/onboarding/copy.js +235 -0
- package/lib/quoxon/onboarding/credential.js +95 -0
- package/lib/quoxon/onboarding/detect.js +22 -0
- package/lib/quoxon/onboarding/finish.js +119 -0
- package/lib/quoxon/onboarding/index.js +67 -0
- package/lib/quoxon/onboarding/installRunner.js +225 -0
- package/lib/quoxon/onboarding/intent.js +53 -0
- package/lib/quoxon/onboarding/journey.js +73 -0
- package/lib/quoxon/onboarding/preflight.js +142 -0
- package/lib/quoxon/onboarding/runner.js +58 -0
- package/lib/quoxon/onboarding/view.js +625 -0
- package/lib/quoxon/print.js +94 -0
- package/lib/quoxon/qart.js +379 -0
- package/lib/quoxon/qglyph.js +143 -0
- package/lib/quoxon/qhello.js +58 -0
- package/lib/quoxon/qmotion.js +111 -0
- package/lib/quoxon/render.js +648 -0
- package/lib/quoxon/roster.js +221 -0
- package/lib/quoxon/route.js +38 -0
- package/lib/quoxon/rules.js +130 -0
- package/lib/quoxon/session.js +68 -0
- package/lib/quoxon/skills.js +30 -0
- package/lib/quoxon/slash/compact.js +90 -0
- package/lib/quoxon/slash/ledger.js +83 -0
- package/lib/quoxon/slash/listView.js +28 -0
- package/lib/quoxon/slash/palette.js +104 -0
- package/lib/quoxon/slash/paletteController.js +59 -0
- package/lib/quoxon/slash/panels.js +101 -0
- package/lib/quoxon/slash/picker.js +38 -0
- package/lib/quoxon/slash/restart.js +144 -0
- package/lib/quoxon/slash/resume.js +59 -0
- package/lib/quoxon/slash/settings.js +50 -0
- package/lib/quoxon/sound/index.js +186 -0
- package/lib/quoxon/sound/mapping.js +119 -0
- package/lib/quoxon/sound/output.js +52 -0
- package/lib/quoxon/sound/synth.js +122 -0
- package/lib/quoxon/term.js +106 -0
- package/lib/quoxon/textBuffer.js +242 -0
- package/lib/quoxon/theme.js +253 -0
- package/lib/quoxon/transcript.js +150 -0
- package/lib/quoxon/web/qface.js +81 -0
- package/lib/quoxon/welcome.js +52 -0
- package/lib/tui/app.jsx +289 -0
- package/lib/tui/components/Breadcrumb.jsx +42 -0
- package/lib/tui/components/ChatInput.jsx +68 -0
- package/lib/tui/components/Confirm.jsx +32 -0
- package/lib/tui/components/DataTable.jsx +270 -0
- package/lib/tui/components/DetailPanel.jsx +241 -0
- package/lib/tui/components/HelpOverlay.jsx +67 -0
- package/lib/tui/components/Layout.jsx +129 -0
- package/lib/tui/components/Palette.jsx +141 -0
- package/lib/tui/components/SearchInput.jsx +125 -0
- package/lib/tui/components/Sparkline.jsx +55 -0
- package/lib/tui/components/StatusBar.jsx +133 -0
- package/lib/tui/components/Toast.jsx +52 -0
- package/lib/tui/hooks/useApi.js +44 -0
- package/lib/tui/hooks/useHealth.js +90 -0
- package/lib/tui/hooks/usePolledApi.js +86 -0
- package/lib/tui/hooks/useTerminalSize.js +46 -0
- package/lib/tui/index.js +56 -0
- package/lib/tui/intro/Intro.jsx +332 -0
- package/lib/tui/jsx-loader.js +32 -0
- package/lib/tui/keys.js +87 -0
- package/lib/tui/registry.js +49 -0
- package/lib/tui/theme.js +103 -0
- package/lib/tui/views/AdminView.jsx +398 -0
- package/lib/tui/views/AgentView.jsx +204 -0
- package/lib/tui/views/BackupView.jsx +225 -0
- package/lib/tui/views/BrainView.jsx +223 -0
- package/lib/tui/views/ChatView.jsx +468 -0
- package/lib/tui/views/FilesView.jsx +218 -0
- package/lib/tui/views/FleetView.jsx +234 -0
- package/lib/tui/views/GovernanceView.jsx +453 -0
- package/lib/tui/views/HealthView.jsx +216 -0
- package/lib/tui/views/InboxView.jsx +228 -0
- package/lib/tui/views/LucidDeck.jsx +727 -0
- package/lib/tui/views/MainMenu.jsx +123 -0
- package/lib/tui/views/MemoryView.jsx +377 -0
- package/lib/tui/views/ObjectivesView.jsx +230 -0
- package/lib/tui/views/OrgView.jsx +229 -0
- package/lib/tui/views/ToolView.jsx +221 -0
- package/lib/tui/views/VaultView.jsx +422 -0
- package/lib/tui/views/WorkflowView.jsx +329 -0
- package/package.json +72 -4
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engagement commands: QUOX-SECURITY Phase 0 engagement/scope object.
|
|
3
|
+
*
|
|
4
|
+
* quox engagement create <name> --environment-tier <tier> [--risk-ceiling <tier>]
|
|
5
|
+
* quox engagement list
|
|
6
|
+
* quox engagement show <id>
|
|
7
|
+
* quox engagement add-target <id> <type> <value> [--verification-method <method>]
|
|
8
|
+
* quox engagement verify-target <id> <targetId>
|
|
9
|
+
*
|
|
10
|
+
* Backend: services/collector/server.js's /api/v1/security/engagements
|
|
11
|
+
* routes (collector-native, own security.db, not a proxy). No agent
|
|
12
|
+
* capability exists yet, this is pure engagement/scope CRUD plus the
|
|
13
|
+
* domain-verification challenge that every future security capability
|
|
14
|
+
* call must pass. See lib/securityEngagement.js for the enforcement
|
|
15
|
+
* itself (environment_tier/risk_ceiling validation, verification, and the
|
|
16
|
+
* policy gate wired into approvalGate.js).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { writeFileSync } from 'node:fs';
|
|
20
|
+
import { formatOutput, formatDetail, printSuccess, printInfo } from '../core/output.js';
|
|
21
|
+
import { events, printEvent, line as glyphLine } from '../core/quoxglyph.js';
|
|
22
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
23
|
+
|
|
24
|
+
export function registerEngagementCommands(program, ctx) {
|
|
25
|
+
const { client, auth } = ctx;
|
|
26
|
+
|
|
27
|
+
const engagement = program
|
|
28
|
+
.command('engagement')
|
|
29
|
+
.description('QUOX-SECURITY: engagements + scope targets, live capability scanners (run via scan/scan-async; see "engagement tools" for the list), and findings (report/verify/regrade/resolve)');
|
|
30
|
+
|
|
31
|
+
engagement
|
|
32
|
+
.command('create <name>')
|
|
33
|
+
.description('Create a new security engagement')
|
|
34
|
+
.requiredOption('--environment-tier <tier>', 'dev | staging | production')
|
|
35
|
+
.option('--risk-ceiling <tier>', 'passive | discovery | safe_active | intrusive | credential_sensitive | exploit_emulation (production caps at discovery in Phase 0)')
|
|
36
|
+
.option('--time-window-start <iso>', 'ISO 8601 timestamp')
|
|
37
|
+
.option('--time-window-end <iso>', 'ISO 8601 timestamp')
|
|
38
|
+
.option('--responsible-project <name>', 'the project/team this engagement belongs to')
|
|
39
|
+
.action(async (name, options) => {
|
|
40
|
+
auth.requireAuth();
|
|
41
|
+
const body = { name, environment_tier: options.environmentTier };
|
|
42
|
+
if (options.riskCeiling) body.risk_ceiling = options.riskCeiling;
|
|
43
|
+
if (options.timeWindowStart || options.timeWindowEnd) {
|
|
44
|
+
body.time_window = { start: options.timeWindowStart || null, end: options.timeWindowEnd || null };
|
|
45
|
+
}
|
|
46
|
+
if (options.responsibleProject) body.responsible_project = options.responsibleProject;
|
|
47
|
+
|
|
48
|
+
const ep = resolveEndpoint('engagement.create');
|
|
49
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
50
|
+
printEvent(events.created('quox', 'engagement', data.engagement.id));
|
|
51
|
+
formatDetail(data.engagement, { format: program.opts().output, title: 'Engagement' });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
engagement
|
|
55
|
+
.command('list')
|
|
56
|
+
.description('List engagements for the current org')
|
|
57
|
+
.action(async () => {
|
|
58
|
+
auth.requireAuth();
|
|
59
|
+
const ep = resolveEndpoint('engagement.list');
|
|
60
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
61
|
+
formatOutput({
|
|
62
|
+
columns: ['id', 'name', 'status', 'environment_tier', 'risk_ceiling', 'created_at'],
|
|
63
|
+
headers: ['ID', 'Name', 'Status', 'Environment', 'Risk Ceiling', 'Created'],
|
|
64
|
+
rows: data.engagements || [],
|
|
65
|
+
}, { format: program.opts().output, emptyMessage: 'No engagements yet.' });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
engagement
|
|
69
|
+
.command('show <id>')
|
|
70
|
+
.description('Show an engagement and its targets')
|
|
71
|
+
.action(async (id) => {
|
|
72
|
+
auth.requireAuth();
|
|
73
|
+
const ep = resolveEndpoint('engagement.get', { id });
|
|
74
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
75
|
+
formatDetail(data.engagement, { format: program.opts().output, title: 'Engagement' });
|
|
76
|
+
formatOutput({
|
|
77
|
+
columns: ['id', 'type', 'value', 'verification_status', 'verified_at'],
|
|
78
|
+
headers: ['ID', 'Type', 'Value', 'Verification', 'Verified At'],
|
|
79
|
+
rows: data.targets || [],
|
|
80
|
+
}, { format: program.opts().output, emptyMessage: 'No targets added yet.' });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
engagement
|
|
84
|
+
.command('set-gripe-target <id>')
|
|
85
|
+
.description('Set which repo a finding filed from this engagement goes to, and which org GitHub connector token files it. Lets a customer-site engagement file into the customer repo with the customer org token. Pass --clear (or --repo "") to fall back to the platform default (quoxai/quox) and the engagement own org.')
|
|
86
|
+
.option('--repo <owner/name>', 'GitHub repo for findings from this engagement (owner/name). Pass an empty value to clear.')
|
|
87
|
+
.option('--connector-org <orgId>', 'org whose GitHub connector token files the issue')
|
|
88
|
+
.option('--clear', 'clear both overrides back to the platform default')
|
|
89
|
+
.action(async (id, options) => {
|
|
90
|
+
auth.requireAuth();
|
|
91
|
+
if (!options.clear && options.repo === undefined && options.connectorOrg === undefined) {
|
|
92
|
+
throw new Error('nothing to set: pass --repo <owner/name>, --connector-org <orgId>, or --clear');
|
|
93
|
+
}
|
|
94
|
+
let gripeRepo = null;
|
|
95
|
+
let connectorOrg = null;
|
|
96
|
+
if (!options.clear) {
|
|
97
|
+
gripeRepo = options.repo ? options.repo : null;
|
|
98
|
+
connectorOrg = options.connectorOrg ? options.connectorOrg : null;
|
|
99
|
+
}
|
|
100
|
+
const ep = resolveEndpoint('engagement.gripe_target.set', { id });
|
|
101
|
+
const data = await client.patch(ep.path, {
|
|
102
|
+
service: ep.service,
|
|
103
|
+
body: { gripe_repo: gripeRepo, gripe_connector_org_id: connectorOrg },
|
|
104
|
+
});
|
|
105
|
+
const eng = data.engagement;
|
|
106
|
+
printSuccess(`Gripe target updated for engagement ${eng.id}.`);
|
|
107
|
+
console.log(` Repo: ${eng.gripe_repo || 'platform default (quoxai/quox)'}`);
|
|
108
|
+
console.log(` Connector org: ${eng.gripe_connector_org_id || 'engagement own org'}`);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
engagement
|
|
112
|
+
.command('add-target <id> <type> <value>')
|
|
113
|
+
.description('Add a target (type: domain | cidr | repo | cloud_account | url) to an engagement')
|
|
114
|
+
.option('--verification-method <method>', 'dns_txt | well_known_file | quoxagent_install (private-network targets, proven via real fleet membership; signed_attestation not implemented in Phase 0)')
|
|
115
|
+
.action(async (id, type, value, options) => {
|
|
116
|
+
auth.requireAuth();
|
|
117
|
+
const body = { type, value };
|
|
118
|
+
if (options.verificationMethod) body.verification_method = options.verificationMethod;
|
|
119
|
+
|
|
120
|
+
const ep = resolveEndpoint('engagement.target.create', { id });
|
|
121
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
122
|
+
printSuccess(`Target "${value}" added to engagement ${id}.`);
|
|
123
|
+
if (data.target.verification_token) {
|
|
124
|
+
if (data.target.verification_method === 'dns_txt') {
|
|
125
|
+
console.log(` Add a DNS TXT record on ${value}: quox-verify=${data.target.verification_token}`);
|
|
126
|
+
} else if (data.target.verification_method === 'well_known_file') {
|
|
127
|
+
console.log(` Serve this content at https://${value}/.well-known/quox-verification: ${data.target.verification_token}`);
|
|
128
|
+
}
|
|
129
|
+
console.log(` Then run: quox engagement verify-target ${id} ${data.target.id}`);
|
|
130
|
+
}
|
|
131
|
+
formatDetail(data.target, { format: program.opts().output, title: 'Target' });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
engagement
|
|
135
|
+
.command('verify-target <id> <targetId>')
|
|
136
|
+
.description('Run the domain-verification challenge for a target')
|
|
137
|
+
.action(async (id, targetId) => {
|
|
138
|
+
auth.requireAuth();
|
|
139
|
+
const ep = resolveEndpoint('engagement.target.verify', { id, targetId });
|
|
140
|
+
const data = await client.post(ep.path, { service: ep.service, body: {} });
|
|
141
|
+
if (data.target.verification_status === 'verified') {
|
|
142
|
+
printEvent(glyphLine('quox', 'target verified', { result: data.target.value, status: 'ok' }));
|
|
143
|
+
} else {
|
|
144
|
+
console.log(` Verification status: ${data.target.verification_status} (challenge did not match yet).`);
|
|
145
|
+
}
|
|
146
|
+
formatDetail(data.target, { format: program.opts().output, title: 'Target' });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
engagement
|
|
150
|
+
.command('check-policy <id> <targetValue> <capabilityRisk>')
|
|
151
|
+
.description('Check whether a capability of the given risk tier is currently allowed against a target (passive|discovery|safe_active|intrusive|credential_sensitive|exploit_emulation)')
|
|
152
|
+
.action(async (id, targetValue, capabilityRisk) => {
|
|
153
|
+
auth.requireAuth();
|
|
154
|
+
const ep = resolveEndpoint('engagement.check_policy', { id });
|
|
155
|
+
const data = await client.post(ep.path, { service: ep.service, body: { target_value: targetValue, capability_risk: capabilityRisk } });
|
|
156
|
+
if (data.allowed) {
|
|
157
|
+
printEvent(glyphLine('quox', 'policy check', { result: data.requiresApproval ? 'allowed · requires approval' : 'allowed', status: data.requiresApproval ? 'warn' : 'ok' }));
|
|
158
|
+
} else {
|
|
159
|
+
console.log(` Denied: ${data.reason}`);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
engagement
|
|
164
|
+
.command('scan <id> <target> <tool>')
|
|
165
|
+
.description('Attempt a Phase 1 capability tool against a verified, in-scope target (tool: discover_hosts | probe_http | discover_app_ports | vuln_scan). Policy-gated; honestly fails if no scan host is configured.')
|
|
166
|
+
.action(async (id, target, tool) => {
|
|
167
|
+
auth.requireAuth();
|
|
168
|
+
const ep = resolveEndpoint('engagement.capability', { id, tool });
|
|
169
|
+
const data = await client.post(ep.path, { service: ep.service, body: { target } });
|
|
170
|
+
if (data.success) {
|
|
171
|
+
printEvent(glyphLine('quox', `${tool}`, { result: 'completed', status: 'ok' }));
|
|
172
|
+
console.log(JSON.stringify(data.output, null, 2));
|
|
173
|
+
} else {
|
|
174
|
+
console.log(` Failed (allowed: ${data.allowed}): ${data.error}`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
engagement
|
|
179
|
+
.command('discover-ports <id> <hostTarget>')
|
|
180
|
+
.description('Discover which app ports on a verified, in-scope host actually serve HTTP(S) (httpx over a curated port set), so a scan hits the real surface instead of only :80/:443. With --add, register each discovered endpoint as a verified url target (control re-proven via fleet membership).')
|
|
181
|
+
.option('--add', 'Add each discovered endpoint as a url target and verify it')
|
|
182
|
+
.action(async (id, hostTarget, options) => {
|
|
183
|
+
auth.requireAuth();
|
|
184
|
+
const ep = resolveEndpoint('engagement.discover_app_ports', { id });
|
|
185
|
+
const data = await client.post(ep.path, { service: ep.service, body: { target: hostTarget, add: options.add === true } });
|
|
186
|
+
if (!data.success) {
|
|
187
|
+
console.log(` Failed (allowed: ${data.allowed}): ${data.error}`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const discovered = data.discovered || [];
|
|
191
|
+
printSuccess(`Discovered ${discovered.length} serving endpoint(s) on ${hostTarget}.`);
|
|
192
|
+
formatOutput({
|
|
193
|
+
columns: ['url', 'status_code', 'title'],
|
|
194
|
+
headers: ['URL', 'Status', 'Title'],
|
|
195
|
+
rows: discovered,
|
|
196
|
+
}, { format: program.opts().output, emptyMessage: 'No serving app ports found.' });
|
|
197
|
+
if (options.add) {
|
|
198
|
+
const added = data.added || [];
|
|
199
|
+
const skipped = data.skipped || [];
|
|
200
|
+
printSuccess(`Added ${added.length} url target(s); skipped ${skipped.length} already in scope.`);
|
|
201
|
+
if (added.length) {
|
|
202
|
+
formatOutput({
|
|
203
|
+
columns: ['url', 'targetId', 'verified'],
|
|
204
|
+
headers: ['URL', 'Target', 'Verified'],
|
|
205
|
+
rows: added,
|
|
206
|
+
}, { format: program.opts().output, emptyMessage: 'None added.' });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
engagement
|
|
212
|
+
.command('tools')
|
|
213
|
+
.description('List the real capability tools (name, risk tier, binary). Quoxcli-parity fix: this catalog backed the dashboard Tools tab but had no CLI verb.')
|
|
214
|
+
.action(async () => {
|
|
215
|
+
auth.requireAuth();
|
|
216
|
+
const ep = resolveEndpoint('engagement.capability_tools');
|
|
217
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
218
|
+
formatOutput({
|
|
219
|
+
columns: ['tool', 'capabilityRisk', 'binary', 'description'],
|
|
220
|
+
headers: ['Tool', 'Risk', 'Binary', 'Description'],
|
|
221
|
+
rows: data.tools || [],
|
|
222
|
+
}, { format: program.opts().output, emptyMessage: 'No capability tools registered.' });
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
engagement
|
|
226
|
+
.command('scan-async <id> <target> <tool>')
|
|
227
|
+
.description('Start a capability tool as a background job and return immediately (job_id). Use for tools that can outrun a single request, e.g. vuln_scan/nuclei or discover_app_ports over a wide host set. Poll with `quox engagement job <jobId>`.')
|
|
228
|
+
.action(async (id, target, tool) => {
|
|
229
|
+
auth.requireAuth();
|
|
230
|
+
const ep = resolveEndpoint('engagement.capability.start', { id, tool });
|
|
231
|
+
const data = await client.post(ep.path, { service: ep.service, body: { target } });
|
|
232
|
+
if (data.success && data.job_id) {
|
|
233
|
+
printSuccess(`${tool} started as job ${data.job_id} (status: ${data.status}).`);
|
|
234
|
+
console.log(` Poll: quox engagement job ${data.job_id}`);
|
|
235
|
+
} else {
|
|
236
|
+
console.log(` Failed (allowed: ${data.allowed}): ${data.error}`);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
engagement
|
|
241
|
+
.command('job <jobId>')
|
|
242
|
+
.description('Check the status/result of an async scan job (queued | running | succeeded | failed)')
|
|
243
|
+
.action(async (jobId) => {
|
|
244
|
+
auth.requireAuth();
|
|
245
|
+
const ep = resolveEndpoint('engagement.job.get', { jobId });
|
|
246
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
247
|
+
formatDetail(data.job, { format: program.opts().output, title: 'Scan job' });
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
engagement
|
|
251
|
+
.command('jobs <id>')
|
|
252
|
+
.description('List async scan jobs for an engagement')
|
|
253
|
+
.action(async (id) => {
|
|
254
|
+
auth.requireAuth();
|
|
255
|
+
const ep = resolveEndpoint('engagement.jobs.list', { id });
|
|
256
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
257
|
+
formatOutput({
|
|
258
|
+
columns: ['id', 'tool', 'target', 'status', 'created_at', 'finished_at'],
|
|
259
|
+
headers: ['ID', 'Tool', 'Target', 'Status', 'Created', 'Finished'],
|
|
260
|
+
rows: data.jobs || [],
|
|
261
|
+
}, { format: program.opts().output, emptyMessage: 'No scan jobs yet.' });
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
engagement
|
|
265
|
+
.command('report-finding <id> <targetValue> <title> <description>')
|
|
266
|
+
.description('Report a candidate finding against a target within an engagement. Always starts as status "candidate"')
|
|
267
|
+
.option('--severity <level>', 'info | low | medium | high | critical', 'info')
|
|
268
|
+
.action(async (id, targetValue, title, description, options) => {
|
|
269
|
+
auth.requireAuth();
|
|
270
|
+
const ep = resolveEndpoint('engagement.findings.report', { id });
|
|
271
|
+
const data = await client.post(ep.path, {
|
|
272
|
+
service: ep.service,
|
|
273
|
+
body: { target_value: targetValue, title, description, severity: options.severity },
|
|
274
|
+
});
|
|
275
|
+
printSuccess(`Finding "${data.finding.id}" reported (status: candidate).`);
|
|
276
|
+
formatDetail(data.finding, { format: program.opts().output, title: 'Finding' });
|
|
277
|
+
// quox#601 report-time reminder: severity defaults to 'info', and a batch
|
|
278
|
+
// left there ships understated on the published /security projection
|
|
279
|
+
// (that is exactly what bit trust-proof cycle 3). Nudge, don't block: a
|
|
280
|
+
// genuinely-info finding is fine, but the default should be a choice.
|
|
281
|
+
if (data.finding?.severity === 'info') {
|
|
282
|
+
printInfo(`Severity is 'info' (the default). If that understates it, re-grade with a reason: quox engagement regrade-finding ${data.finding.id} <severity> --reason "<why>"`);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
engagement
|
|
287
|
+
.command('findings <id>')
|
|
288
|
+
.description('List findings for an engagement')
|
|
289
|
+
.action(async (id) => {
|
|
290
|
+
auth.requireAuth();
|
|
291
|
+
const ep = resolveEndpoint('engagement.findings.list', { id });
|
|
292
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
293
|
+
const findings = data.findings || [];
|
|
294
|
+
// Surface gripe_url/quoxplan_ref columns only when at least one
|
|
295
|
+
// finding actually has a value — an all-empty column is just noise.
|
|
296
|
+
const hasGripe = findings.some((f) => f.gripe_url);
|
|
297
|
+
const hasQuoxplan = findings.some((f) => f.quoxplan_ref);
|
|
298
|
+
const columns = ['id', 'title', 'severity', 'status', 'target_value', 'created_at'];
|
|
299
|
+
const headers = ['ID', 'Title', 'Severity', 'Status', 'Target', 'Created'];
|
|
300
|
+
if (hasGripe) { columns.push('gripe_url'); headers.push('Issue'); }
|
|
301
|
+
if (hasQuoxplan) { columns.push('quoxplan_ref'); headers.push('QuoxPlan'); }
|
|
302
|
+
formatOutput({
|
|
303
|
+
columns,
|
|
304
|
+
headers,
|
|
305
|
+
rows: findings,
|
|
306
|
+
}, { format: program.opts().output, emptyMessage: 'No findings yet.' });
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
engagement
|
|
310
|
+
.command('verify-finding <findingId> <verdict>')
|
|
311
|
+
.description('Confirm or refute a candidate finding (verdict: confirmed | refuted). A reason is required, a bare verdict is refused')
|
|
312
|
+
.requiredOption('--reason <text>', 'why this verdict was reached')
|
|
313
|
+
.action(async (findingId, verdict, options) => {
|
|
314
|
+
auth.requireAuth();
|
|
315
|
+
const ep = resolveEndpoint('engagement.findings.verify', { findingId });
|
|
316
|
+
const data = await client.post(ep.path, { service: ep.service, body: { verdict, reason: options.reason } });
|
|
317
|
+
printSuccess(`Finding "${data.finding.id}" ${data.finding.status}.`);
|
|
318
|
+
formatDetail(data.finding, { format: program.opts().output, title: 'Finding' });
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
engagement
|
|
322
|
+
.command('resolve-finding <findingId>')
|
|
323
|
+
.description('Mark a finding remediated (status: resolved). Refuses a refuted finding')
|
|
324
|
+
.action(async (findingId) => {
|
|
325
|
+
auth.requireAuth();
|
|
326
|
+
const ep = resolveEndpoint('engagement.findings.resolve', { findingId });
|
|
327
|
+
const data = await client.post(ep.path, { service: ep.service, body: {} });
|
|
328
|
+
printSuccess(`Finding "${data.finding.id}" ${data.finding.status}.`);
|
|
329
|
+
formatDetail(data.finding, { format: program.opts().output, title: 'Finding' });
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
engagement
|
|
333
|
+
.command('regrade-finding <findingId> <severity>')
|
|
334
|
+
.description('Re-grade a reported finding\'s severity (info | low | medium | high | critical). A reason is required, the same discipline as verify-finding, because severity is a published field')
|
|
335
|
+
.requiredOption('--reason <text>', 'why the severity is being changed')
|
|
336
|
+
.action(async (findingId, severity, options) => {
|
|
337
|
+
auth.requireAuth();
|
|
338
|
+
const ep = resolveEndpoint('engagement.findings.regrade', { findingId });
|
|
339
|
+
const data = await client.post(ep.path, { service: ep.service, body: { severity, reason: options.reason } });
|
|
340
|
+
const f = data.finding;
|
|
341
|
+
printSuccess(`Finding "${f.id}" re-graded ${f.severity_from} -> ${f.severity}.`);
|
|
342
|
+
formatDetail(f, { format: program.opts().output, title: 'Finding' });
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Finding -> next-steps pipeline: lib/securityReportActions.js. Raise
|
|
346
|
+
// issue (GitHub issue) / Propose as QuoxPlan / Download PDF, mirroring the
|
|
347
|
+
// dashboard Findings tab's action bar so nothing ships dashboard-only
|
|
348
|
+
// (quoxcli-parity gate).
|
|
349
|
+
|
|
350
|
+
// Known output-format values understood by the global -o/--output flag
|
|
351
|
+
// (bin/quox.js). The `report` command below reuses -o/--output for a
|
|
352
|
+
// different purpose (destination file path), which collides with that
|
|
353
|
+
// global flag at the root parser — a real path passed after `report <id>`
|
|
354
|
+
// lands in `program.opts().output` rather than this command's own
|
|
355
|
+
// `options.output`. Anything that isn't a recognized format name is
|
|
356
|
+
// treated as the destination file rather than silently dropped.
|
|
357
|
+
const KNOWN_OUTPUT_FORMATS = new Set(['table', 'json', 'csv', 'quiet']);
|
|
358
|
+
|
|
359
|
+
engagement
|
|
360
|
+
.command('report <id>')
|
|
361
|
+
.description('Download the engagement report (targets, tools run, findings) as a PDF')
|
|
362
|
+
.option('--pdf', 'Format flag — PDF is the only format currently shipped, kept for forward compatibility')
|
|
363
|
+
.option('--output <file>', 'Output file path (default: quox-security-<id>.pdf)')
|
|
364
|
+
.action(async (id, options) => {
|
|
365
|
+
auth.requireAuth();
|
|
366
|
+
const ep = resolveEndpoint('engagement.report.pdf', { id });
|
|
367
|
+
const res = await client.get(ep.path, { service: ep.service, raw: true });
|
|
368
|
+
if (!res.ok) {
|
|
369
|
+
let data;
|
|
370
|
+
try { data = await res.json(); } catch { data = { error: res.statusText }; }
|
|
371
|
+
throw new Error(data.error || `Request failed with status ${res.status}`);
|
|
372
|
+
}
|
|
373
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
374
|
+
const rootOutput = program.opts().output;
|
|
375
|
+
const outFile = options.output
|
|
376
|
+
|| (rootOutput && !KNOWN_OUTPUT_FORMATS.has(rootOutput) ? rootOutput : null)
|
|
377
|
+
|| `quox-security-${id}.pdf`;
|
|
378
|
+
writeFileSync(outFile, buf);
|
|
379
|
+
printSuccess(`Report saved to ${outFile} (${buf.length} bytes).`);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
async function raiseIssue(findingId) {
|
|
383
|
+
auth.requireAuth();
|
|
384
|
+
const ep = resolveEndpoint('engagement.findings.gripe', { findingId });
|
|
385
|
+
const data = await client.post(ep.path, { service: ep.service, body: {} });
|
|
386
|
+
if (data.configured === false) {
|
|
387
|
+
console.log(` ${data.message || 'GitHub not configured.'}`);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (data.ok === false) {
|
|
391
|
+
console.log(` ${data.message || 'GitHub connection rejected.'}`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (data.alreadyFiled) {
|
|
395
|
+
console.log(` Already filed: ${data.url}`);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
printSuccess(`Filed ${data.url} (repo: ${data.repo}).`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
engagement
|
|
402
|
+
.command('issue <findingId>')
|
|
403
|
+
.description('Raise a finding as a GitHub issue. Honest no-op if GitHub is not configured or the stored token is rejected, never a fake success. Skips re-filing if already raised.')
|
|
404
|
+
.action(raiseIssue);
|
|
405
|
+
|
|
406
|
+
// Deprecated alias, kept working (not advertised) so nothing that
|
|
407
|
+
// already scripted `quox engagement gripe` breaks.
|
|
408
|
+
engagement
|
|
409
|
+
.command('gripe <findingId>', { hidden: true })
|
|
410
|
+
.description('Deprecated alias for "quox engagement issue"')
|
|
411
|
+
.action(raiseIssue);
|
|
412
|
+
|
|
413
|
+
engagement
|
|
414
|
+
.command('propose <findingId>')
|
|
415
|
+
.description('Propose a finding as a QuoxPlan note (default stream: QUOX-SECURITY, #438)')
|
|
416
|
+
.option('--stream-id <id>', 'QuoxPlan stream id to append the note to (default QUOX-SECURITY)')
|
|
417
|
+
.action(async (findingId, options) => {
|
|
418
|
+
auth.requireAuth();
|
|
419
|
+
const ep = resolveEndpoint('engagement.findings.propose', { findingId });
|
|
420
|
+
const body = options.streamId ? { stream_id: options.streamId } : {};
|
|
421
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
422
|
+
printSuccess(`Proposed on ${data.streamId} (note ${data.note?.id ?? '—'}).`);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// QUOX-SECURITY-SCHED Phase 4: recurring scan schedules — a standing
|
|
426
|
+
// instruction to re-run a tool against a target on a fixed cadence,
|
|
427
|
+
// owned by an engagement. Backend: lib/securityEngagementSchedules.js
|
|
428
|
+
// (storage) + lib/securityScanScheduler.js (the server-side loop that
|
|
429
|
+
// actually fires due schedules). MIN_INTERVAL_SECONDS=300 is enforced
|
|
430
|
+
// server-side; a too-short --interval fails at create, not silently.
|
|
431
|
+
const schedule = engagement
|
|
432
|
+
.command('schedule')
|
|
433
|
+
.description('Recurring scan schedules (re-run a tool on a fixed cadence)');
|
|
434
|
+
|
|
435
|
+
schedule
|
|
436
|
+
.command('create <engagementId> <target> <tool>')
|
|
437
|
+
.description('Create a recurring schedule (tool: bare capability name, e.g. vuln_scan)')
|
|
438
|
+
.option('--interval <seconds>', 'interval cadence in seconds (minimum 300)')
|
|
439
|
+
.option('--cron <expr>', 'cron cadence, a 5-field UTC expression e.g. "0 3 * * 3" for weekly Wed 03:00')
|
|
440
|
+
.action(async (engagementId, target, tool, options) => {
|
|
441
|
+
auth.requireAuth();
|
|
442
|
+
if (options.interval && options.cron) {
|
|
443
|
+
throw new Error('--interval and --cron are mutually exclusive; pass exactly one cadence');
|
|
444
|
+
}
|
|
445
|
+
if (!options.interval && !options.cron) {
|
|
446
|
+
throw new Error('a cadence is required: pass --interval <seconds> or --cron "<expr>"');
|
|
447
|
+
}
|
|
448
|
+
const body = { target_value: target, tool };
|
|
449
|
+
if (options.cron) {
|
|
450
|
+
body.schedule_kind = 'cron';
|
|
451
|
+
body.cron_expression = options.cron;
|
|
452
|
+
} else {
|
|
453
|
+
const intervalSeconds = parseInt(options.interval, 10);
|
|
454
|
+
if (!Number.isInteger(intervalSeconds) || String(intervalSeconds) !== String(options.interval).trim()) {
|
|
455
|
+
throw new Error(`--interval must be an integer number of seconds, got "${options.interval}"`);
|
|
456
|
+
}
|
|
457
|
+
body.interval_seconds = intervalSeconds;
|
|
458
|
+
}
|
|
459
|
+
const ep = resolveEndpoint('engagement.schedule.create', { id: engagementId });
|
|
460
|
+
const data = await client.post(ep.path, {
|
|
461
|
+
service: ep.service,
|
|
462
|
+
body,
|
|
463
|
+
});
|
|
464
|
+
printSuccess(`Schedule "${data.schedule.id}" created (next run: ${data.schedule.next_run_at}).`);
|
|
465
|
+
formatDetail(data.schedule, { format: program.opts().output, title: 'Schedule' });
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
schedule
|
|
469
|
+
.command('list <engagementId>')
|
|
470
|
+
.description('List schedules for an engagement')
|
|
471
|
+
.action(async (engagementId) => {
|
|
472
|
+
auth.requireAuth();
|
|
473
|
+
const ep = resolveEndpoint('engagement.schedule.list', { id: engagementId });
|
|
474
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
475
|
+
formatOutput({
|
|
476
|
+
columns: ['id', 'target_value', 'tool', 'interval_seconds', 'enabled', 'next_run_at', 'last_status'],
|
|
477
|
+
headers: ['ID', 'Target', 'Tool', 'Interval (s)', 'Enabled', 'Next Run', 'Last Status'],
|
|
478
|
+
rows: data.schedules || [],
|
|
479
|
+
}, { format: program.opts().output, emptyMessage: 'No schedules yet.' });
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
schedule
|
|
483
|
+
.command('show <scheduleId>')
|
|
484
|
+
.description('Show a schedule, including its last diff summary')
|
|
485
|
+
.action(async (scheduleId) => {
|
|
486
|
+
auth.requireAuth();
|
|
487
|
+
const ep = resolveEndpoint('engagement.schedule.get', { scheduleId });
|
|
488
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
489
|
+
formatDetail(data.schedule, { format: program.opts().output, title: 'Schedule' });
|
|
490
|
+
if (data.schedule?.last_diff) {
|
|
491
|
+
console.log(` Last diff: +${data.schedule.last_diff.addedCount ?? 0} -${data.schedule.last_diff.removedCount ?? 0} (${data.schedule.last_diff.newHighSeverity?.length ?? 0} new high-severity)`);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
schedule
|
|
496
|
+
.command('update <scheduleId>')
|
|
497
|
+
.description('Edit a schedule: change its cadence (--interval or --cron), target, tool, or enabled state')
|
|
498
|
+
.option('--interval <seconds>', 'switch to interval cadence in seconds (minimum 300)')
|
|
499
|
+
.option('--cron <expr>', 'switch to cron cadence, a 5-field UTC expression e.g. "0 3 * * 3"')
|
|
500
|
+
.option('--target <value>', 'change the target value (must be a verified, in-scope target)')
|
|
501
|
+
.option('--tool <name>', 'change the tool (bare capability name, e.g. vuln_scan)')
|
|
502
|
+
.option('--enable', 'enable the schedule')
|
|
503
|
+
.option('--disable', 'disable the schedule')
|
|
504
|
+
.action(async (scheduleId, options) => {
|
|
505
|
+
auth.requireAuth();
|
|
506
|
+
if (options.interval && options.cron) {
|
|
507
|
+
throw new Error('--interval and --cron are mutually exclusive; pass at most one cadence');
|
|
508
|
+
}
|
|
509
|
+
if (options.enable && options.disable) {
|
|
510
|
+
throw new Error('--enable and --disable are mutually exclusive');
|
|
511
|
+
}
|
|
512
|
+
const body = {};
|
|
513
|
+
if (options.cron) {
|
|
514
|
+
body.schedule_kind = 'cron';
|
|
515
|
+
body.cron_expression = options.cron;
|
|
516
|
+
} else if (options.interval) {
|
|
517
|
+
const intervalSeconds = parseInt(options.interval, 10);
|
|
518
|
+
if (!Number.isInteger(intervalSeconds) || String(intervalSeconds) !== String(options.interval).trim()) {
|
|
519
|
+
throw new Error(`--interval must be an integer number of seconds, got "${options.interval}"`);
|
|
520
|
+
}
|
|
521
|
+
body.schedule_kind = 'interval';
|
|
522
|
+
body.interval_seconds = intervalSeconds;
|
|
523
|
+
}
|
|
524
|
+
if (options.target) body.target_value = options.target;
|
|
525
|
+
if (options.tool) body.tool = options.tool;
|
|
526
|
+
if (options.enable) body.enabled = true;
|
|
527
|
+
if (options.disable) body.enabled = false;
|
|
528
|
+
if (Object.keys(body).length === 0) {
|
|
529
|
+
throw new Error('nothing to update: pass at least one of --interval, --cron, --target, --tool, --enable, --disable');
|
|
530
|
+
}
|
|
531
|
+
const ep = resolveEndpoint('engagement.schedule.update', { scheduleId });
|
|
532
|
+
const data = await client.patch(ep.path, { service: ep.service, body });
|
|
533
|
+
printSuccess(`Schedule "${data.schedule.id}" updated (next run: ${data.schedule.next_run_at}).`);
|
|
534
|
+
formatDetail(data.schedule, { format: program.opts().output, title: 'Schedule' });
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
schedule
|
|
538
|
+
.command('enable <scheduleId>')
|
|
539
|
+
.description('Re-enable a schedule')
|
|
540
|
+
.action(async (scheduleId) => {
|
|
541
|
+
auth.requireAuth();
|
|
542
|
+
const ep = resolveEndpoint('engagement.schedule.enable', { scheduleId });
|
|
543
|
+
const data = await client.post(ep.path, { service: ep.service, body: {} });
|
|
544
|
+
printSuccess(`Schedule "${data.schedule.id}" enabled (next run: ${data.schedule.next_run_at}).`);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
schedule
|
|
548
|
+
.command('disable <scheduleId>')
|
|
549
|
+
.description('Disable a schedule (stops firing; existing history kept)')
|
|
550
|
+
.action(async (scheduleId) => {
|
|
551
|
+
auth.requireAuth();
|
|
552
|
+
const ep = resolveEndpoint('engagement.schedule.disable', { scheduleId });
|
|
553
|
+
const data = await client.post(ep.path, { service: ep.service, body: {} });
|
|
554
|
+
printSuccess(`Schedule "${data.schedule.id}" disabled.`);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
schedule
|
|
558
|
+
.command('delete <scheduleId>')
|
|
559
|
+
.description('Delete a schedule')
|
|
560
|
+
.action(async (scheduleId) => {
|
|
561
|
+
auth.requireAuth();
|
|
562
|
+
const ep = resolveEndpoint('engagement.schedule.delete', { scheduleId });
|
|
563
|
+
await client.delete(ep.path, { service: ep.service });
|
|
564
|
+
printSuccess(`Schedule "${scheduleId}" deleted.`);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
engagement
|
|
568
|
+
.command('sync-issues <id>')
|
|
569
|
+
.description('Reconcile this engagement\'s filed findings against their GitHub issue state. A closed issue auto-resolves its finding. Honest no-op if GitHub is not configured')
|
|
570
|
+
.action(async (id) => {
|
|
571
|
+
auth.requireAuth();
|
|
572
|
+
const ep = resolveEndpoint('engagement.sync_issue_states', { id });
|
|
573
|
+
const data = await client.post(ep.path, { service: ep.service, body: {} });
|
|
574
|
+
if (data.configured === false) {
|
|
575
|
+
console.log(` ${data.message || 'GitHub not configured.'}`);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
printSuccess(`Checked ${data.checked ?? 0}, resolved ${data.resolved?.length ?? 0}.`);
|
|
579
|
+
if (data.resolved?.length) {
|
|
580
|
+
console.log(` Resolved: ${data.resolved.join(', ')}`);
|
|
581
|
+
}
|
|
582
|
+
if (data.errors?.length) {
|
|
583
|
+
console.log(` Errors: ${data.errors.map((e) => `${e.findingId} (${e.reason})`).join(', ')}`);
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}
|