Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,11 +159,12 @@ export DRATA_DEFAULT_VERSION=v2

For local use, copy `.env.example` to `.env.local` and fill in `DRATA_API_KEY`. The CLI automatically loads `.env.local` and `.env` from the current working directory without overriding already-exported environment variables.

For stronger local secret storage on macOS, store the key in Keychain:
For stronger local secret storage on macOS, store the key in Keychain and optionally validate it against the API:

```bash
drata auth login --api-key-stdin
drata auth status
drata auth check --json
drata auth logout
```

Expand Down Expand Up @@ -212,6 +213,21 @@ drata describe v2 get-company --json
drata agent-schema v2 --search company
```

## Curated compliance workflows

In addition to raw OpenAPI operations, the CLI includes read-only workflow commands for common compliance triage. These commands use the same auth, region, retry, timeout, JSON, and compact flags as request commands.

```bash
drata summary --json --compact
drata controls failing --json --compact
drata monitors failing --json --compact
drata connections list --status DISCONNECTED --json --compact
drata personnel issues --json --compact
drata evidence expiring --days 60 --json --compact
```

These workflows use v1 list endpoints where they provide workspace-independent compliance rollups and automatically follow page/limit pagination. `--limit N` caps displayed items in workflow outputs without changing the underlying summary counts or API page size. Use `--max-pages N` to bound collection work for very large tenants.

## Invoke operations

### Simple GET
Expand Down Expand Up @@ -393,6 +409,8 @@ autoload -Uz compinit && compinit
- `--dry-run`
- `--read-only`
- `--json`
- `--compact` for curated workflow commands
- `--limit 10` for curated workflow displayed items
- `--retry 2`
- `--timeout-ms 30000`

Expand Down
194 changes: 192 additions & 2 deletions src/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ import { parseRequestFlags, parseSimpleFlags } from "./lib/args.mjs";
import { loadDefaultEnvFiles } from "./lib/env.mjs";
import { invokeOperation, prepareRequest, resolveEffectiveRequestFlags, serializePreparedRequest } from "./lib/http.mjs";
import { renderCompletionScript, runCompletion } from "./lib/completion.mjs";
import {
prepareWorkflowFlags,
printWorkflowPayload,
runConnectionsList,
runControlsFailing,
runEvidenceExpiring,
runMonitorsFailing,
runPersonnelIssues,
runSummary,
runWorkflowOperation,
} from "./lib/workflows.mjs";
import {
filterOperations,
getRegistry,
Expand Down Expand Up @@ -37,7 +48,13 @@ Usage:
drata ops [v1|v2] [--tag TAG] [--search TEXT]
drata describe [v1|v2] <operation>
drata call <v1|v2> <operation> [flags]
drata auth <login|status|logout>
drata auth <login|status|check|logout>
drata summary [--json] [--compact]
drata controls failing [--json] [--compact]
drata monitors failing [--json] [--compact]
drata connections list [--status STATUS] [--json] [--compact]
drata personnel issues [--json] [--compact]
drata evidence expiring [--days N] [--json] [--compact]
drata completion <bash|zsh|fish>
drata agent-schema [v1|v2] [--tag TAG] [--search TEXT]
drata <operation> [flags]
Expand Down Expand Up @@ -65,6 +82,8 @@ Flags for request commands:
--dry-run
--read-only
--json
--compact
--limit 10
--retry 2
--timeout-ms 30000

Expand All @@ -73,6 +92,11 @@ Examples:
drata describe get-company
drata describe v2 get-company
drata auth status
drata auth check --json
drata summary --json --compact
drata controls failing --json --compact
drata monitors failing --json --compact
drata connections list --status DISCONNECTED --json --compact
drata completion zsh
drata agent-schema v2 --search controls
drata get-company
Expand Down Expand Up @@ -157,6 +181,30 @@ async function handleAuth(args) {
return;
}

if (subcommand === "check") {
const flags = await prepareWorkflowFlags(await resolveEffectiveRequestFlags(parseRequestFlags(rest)));
const { result, operation } = await runWorkflowOperation("v2", "get-company", flags);
const payload = {
authenticated: true,
source: flags.apiKeySource,
region: flags.region ?? process.env.DRATA_REGION ?? "us",
operation: serializeOperationSummary(operation),
company: result.data,
};

if (flags.json) {
console.log(JSON.stringify(payload, null, 2));
return;
}

const name = result.data?.name ?? result.data?.companyName ?? result.data?.data?.name ?? "unknown";
console.log(`OK Authenticated`);
console.log(`Company: ${name}`);
console.log(`Region: ${payload.region}`);
console.log(`Key from: ${payload.source}`);
return;
}

if (subcommand === "login") {
const flags = parseRequestFlags(rest);
const { apiKey, source } = await resolveApiKey(flags);
Expand Down Expand Up @@ -198,7 +246,7 @@ async function handleAuth(args) {
return;
}

fail("unknown_auth_command", `Unknown auth command "${subcommand}". Expected login, status, or logout.`, {
fail("unknown_auth_command", `Unknown auth command "${subcommand}". Expected login, status, check, or logout.`, {
command: subcommand,
});
}
Expand Down Expand Up @@ -449,6 +497,118 @@ async function writeResponseOutput(parsedFlags, result) {
};
}

function takeWorkflowNamedFlag(flags, name) {
const values = flags.named.get(name) ?? [];
flags.named.delete(name);
return values.at(-1) ?? null;
}

async function parseWorkflowRequestFlags(args) {
return prepareWorkflowFlags(await resolveEffectiveRequestFlags(parseRequestFlags(args)));
}

async function handleSummary(args) {
const flags = await parseWorkflowRequestFlags(args);
printWorkflowPayload(await runSummary(flags), flags);
}

async function handleControlsWorkflow(args) {
const [subcommand, ...rest] = args;
if (!subcommand || subcommand === "--help" || subcommand === "help") {
printUsage();
return;
}

const flags = await parseWorkflowRequestFlags(rest);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate workflow subcommand before resolving flags

This handler resolves workflow flags (parseWorkflowRequestFlags) before verifying the subcommand, and parseWorkflowRequestFlags performs auth checks. As a result, unauthenticated users see missing_api_key for typos like drata controls nope (and for ... --help) instead of the intended unknown-command/help response, which blocks basic command discovery when not logged in.

Useful? React with 👍 / 👎.

if (subcommand === "failing") {
printWorkflowPayload(await runControlsFailing(flags), flags);
return;
}

fail("unknown_controls_command", `Unknown controls command "${subcommand}". Expected failing.`, {
command: subcommand,
});
}

async function handleMonitorsWorkflow(args) {
const [subcommand, ...rest] = args;
if (!subcommand || subcommand === "--help" || subcommand === "help") {
printUsage();
return;
}

const flags = await parseWorkflowRequestFlags(rest);
if (subcommand === "failing") {
printWorkflowPayload(await runMonitorsFailing(flags), flags);
return;
}

fail("unknown_monitors_command", `Unknown monitors command "${subcommand}". Expected failing.`, {
command: subcommand,
});
}

async function handleConnectionsWorkflow(args) {
const [subcommand, ...rest] = args;
if (!subcommand || subcommand === "--help" || subcommand === "help") {
printUsage();
return;
}

const flags = await parseWorkflowRequestFlags(rest);
const status = takeWorkflowNamedFlag(flags, "status");
if (subcommand === "list") {
printWorkflowPayload(await runConnectionsList(flags, { status }), flags);
return;
}

fail("unknown_connections_command", `Unknown connections command "${subcommand}". Expected list.`, {
command: subcommand,
});
}

async function handlePersonnelWorkflow(args) {
const [subcommand, ...rest] = args;
if (!subcommand || subcommand === "--help" || subcommand === "help") {
printUsage();
return;
}

const flags = await parseWorkflowRequestFlags(rest);
if (subcommand === "issues") {
printWorkflowPayload(await runPersonnelIssues(flags), flags);
return;
}

fail("unknown_personnel_command", `Unknown personnel command "${subcommand}". Expected issues.`, {
command: subcommand,
});
}

async function handleEvidenceWorkflow(args) {
const [subcommand, ...rest] = args;
if (!subcommand || subcommand === "--help" || subcommand === "help") {
printUsage();
return;
}

const flags = await parseWorkflowRequestFlags(rest);
const days = Number(takeWorkflowNamedFlag(flags, "days") ?? 30);
const workspaceId = takeWorkflowNamedFlag(flags, "workspace-id");
if (!Number.isInteger(days) || days < 0) {
fail("invalid_days", `--days must be a non-negative integer`, { days });
}

if (subcommand === "expiring") {
printWorkflowPayload(await runEvidenceExpiring(flags, { days, workspaceId }), flags);
return;
Comment on lines +643 to +664
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new evidence expiring workflow path (including --days validation and optional --workspace-id) isn’t covered by the added CLI integration tests, while other new workflows (auth check, summary, connections list, dry-run rejection) are. Adding at least one test that stubs the workspace lookup + evidence listing and asserts filtering/--days behavior would help prevent regressions.

Copilot uses AI. Check for mistakes.
}

fail("unknown_evidence_command", `Unknown evidence command "${subcommand}". Expected expiring.`, {
command: subcommand,
});
}

async function handleAutoCall(operationInput, args) {
const operation = await resolveOperationAcrossVersions(operationInput, {
preferredVersion: getPreferredVersion(),
Expand Down Expand Up @@ -511,6 +671,36 @@ async function main() {
return;
}

if (command === "summary") {
await handleSummary(rest);
return;
}

if (command === "controls") {
await handleControlsWorkflow(rest);
return;
}

if (command === "monitors") {
await handleMonitorsWorkflow(rest);
return;
}

if (command === "connections") {
await handleConnectionsWorkflow(rest);
return;
}

if (command === "personnel") {
await handlePersonnelWorkflow(rest);
return;
}

if (command === "evidence") {
await handleEvidenceWorkflow(rest);
return;
}

if (command === "completion") {
await handleCompletion(rest);
return;
Expand Down
15 changes: 15 additions & 0 deletions src/lib/args.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export function parseRequestFlags(tokens) {
dryRun: false,
readOnly: false,
json: false,
compact: false,
limit: 0,
retry: 0,
retryProvided: false,
timeoutMs: 30000,
Expand Down Expand Up @@ -196,6 +198,19 @@ export function parseRequestFlags(tokens) {
case "json":
parsed.json = true;
break;
case "compact":
parsed.compact = true;
break;
case "limit": {
const result = readOptionValue(tokens, index, inlineValue, flagName);
parsed.limit = Number(result.value);
if (!Number.isInteger(parsed.limit) || parsed.limit < 0) {
fail("invalid_limit", `--limit must be a non-negative integer`, { value: result.value });
}
pushValue(parsed.named, "limit", result.value);
index = result.nextIndex;
break;
}
case "help":
parsed.help = true;
break;
Expand Down
Loading
Loading