autotel-cli 0.8.14 → 0.9.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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { Command as Command11 } from "commander";
4
+ import { Command as Command12 } from "commander";
5
5
 
6
6
  // src/commands/init.ts
7
7
  import * as fs4 from "fs";
@@ -4381,6 +4381,12 @@ var INVESTIGATE_COMMANDS = [
4381
4381
  investigateCmd("llm expensive", "Top token-spend traces"),
4382
4382
  investigateCmd("llm slow", "Slowest LLM traces"),
4383
4383
  investigateCmd("llm tools", "Tool/function spans grouped by tool name"),
4384
+ investigateCmd("security", "Security telemetry (parent)"),
4385
+ investigateCmd(
4386
+ "security summary",
4387
+ "Security posture: events by severity/category, probe signals, denied responses"
4388
+ ),
4389
+ investigateCmd("security events", "List spans carrying security.* events"),
4384
4390
  investigateCmd("semconv", "Semantic conventions lookup (parent)", { static: true }),
4385
4391
  investigateCmd("semconv list", "List semconv namespaces", { static: true, network: true }),
4386
4392
  investigateCmd("semconv get", "Groups for one namespace", {
@@ -4720,6 +4726,16 @@ function staticFlagsFromOpts(opts) {
4720
4726
  function addTimeWindowFlags(cmd) {
4721
4727
  return cmd.option("--service-name <name>", "Filter by service name").option("--operation-name <name>", "Filter by operation name").option("--lookback-minutes <n>", "Lookback window in minutes", intArg).option("--from <iso>", "Start time (ISO 8601)").option("--to <iso>", "End time (ISO 8601)").option("--limit <n>", "Max results", intArg);
4722
4728
  }
4729
+ function windowFlagsFromOpts(opts) {
4730
+ return {
4731
+ serviceName: opts.serviceName,
4732
+ operationName: opts.operationName,
4733
+ lookbackMinutes: opts.lookbackMinutes,
4734
+ from: opts.from,
4735
+ to: opts.to,
4736
+ limit: opts.limit
4737
+ };
4738
+ }
4723
4739
 
4724
4740
  // src/commands/investigate/health.ts
4725
4741
  async function runHealth(flags) {
@@ -5782,14 +5798,233 @@ function registerCollectorCommands(program) {
5782
5798
  program.addCommand(collectorCmd);
5783
5799
  }
5784
5800
 
5801
+ // src/commands/investigate/security.ts
5802
+ import { Command as Command11 } from "commander";
5803
+ import { toSpanSearchQuery as toSpanSearchQuery2 } from "autotel-mcp";
5804
+ var DEFAULT_LIMIT = 500;
5805
+ var DEFAULT_DENIED_STATUSES = [401, 403, 429];
5806
+ var SAMPLE_TRACE_IDS = 10;
5807
+ function countBy(spans, tagKey) {
5808
+ const counts = {};
5809
+ for (const span of spans) {
5810
+ const value = span.tags[tagKey];
5811
+ if (value === void 0) continue;
5812
+ const key = String(value);
5813
+ counts[key] = (counts[key] ?? 0) + 1;
5814
+ }
5815
+ return counts;
5816
+ }
5817
+ function topEntries(counts, n) {
5818
+ return Object.entries(counts).toSorted((a, b) => b[1] - a[1]).slice(0, n).map(([value, count]) => ({ value, count }));
5819
+ }
5820
+ function sampleTraceIds(spans) {
5821
+ const ids = /* @__PURE__ */ new Set();
5822
+ for (const span of spans) {
5823
+ ids.add(span.traceId);
5824
+ if (ids.size >= SAMPLE_TRACE_IDS) break;
5825
+ }
5826
+ return [...ids];
5827
+ }
5828
+ function dedupeSpans(...lists) {
5829
+ const seen = /* @__PURE__ */ new Set();
5830
+ const merged = [];
5831
+ for (const list of lists) {
5832
+ for (const span of list) {
5833
+ const key = `${span.traceId}:${span.spanId}`;
5834
+ if (seen.has(key)) continue;
5835
+ seen.add(key);
5836
+ merged.push(span);
5837
+ }
5838
+ }
5839
+ return merged;
5840
+ }
5841
+ function countByService(spans) {
5842
+ const counts = {};
5843
+ for (const span of spans) {
5844
+ counts[span.serviceName] = (counts[span.serviceName] ?? 0) + 1;
5845
+ }
5846
+ return counts;
5847
+ }
5848
+ function readStatus(span) {
5849
+ const value = span.tags["http.response.status_code"] ?? span.tags["http.status_code"];
5850
+ if (typeof value === "number") return value;
5851
+ if (typeof value === "string") {
5852
+ const parsed = Number.parseInt(value, 10);
5853
+ if (!Number.isNaN(parsed)) return parsed;
5854
+ }
5855
+ return void 0;
5856
+ }
5857
+ async function runSecuritySummary(flags) {
5858
+ await runInvestigate("security summary", flags, async (backend) => {
5859
+ const limit = flags.limit ?? DEFAULT_LIMIT;
5860
+ const deniedStatuses = flags.deniedStatuses ?? DEFAULT_DENIED_STATUSES;
5861
+ const base = toSpanSearchQuery2({
5862
+ serviceName: flags.serviceName,
5863
+ lookbackMinutes: flags.lookbackMinutes,
5864
+ from: flags.from,
5865
+ to: flags.to,
5866
+ limit
5867
+ });
5868
+ const statusValues = deniedStatuses;
5869
+ const [events, suspicious, deniedNew, deniedLegacy] = await Promise.all([
5870
+ backend.searchSpans({
5871
+ ...base,
5872
+ filters: [{ field: "security.event", operator: "exists" }]
5873
+ }),
5874
+ backend.searchSpans({
5875
+ ...base,
5876
+ filters: [
5877
+ { field: "security.suspicious_request", operator: "exists" }
5878
+ ]
5879
+ }),
5880
+ backend.searchSpans({
5881
+ ...base,
5882
+ filters: [
5883
+ {
5884
+ field: "http.response.status_code",
5885
+ operator: "in",
5886
+ value: statusValues,
5887
+ valueType: "number"
5888
+ }
5889
+ ]
5890
+ }),
5891
+ backend.searchSpans({
5892
+ ...base,
5893
+ filters: [
5894
+ {
5895
+ field: "http.status_code",
5896
+ operator: "in",
5897
+ value: statusValues,
5898
+ valueType: "number"
5899
+ }
5900
+ ]
5901
+ })
5902
+ ]);
5903
+ const denied = dedupeSpans(deniedNew.items, deniedLegacy.items);
5904
+ const deniedByStatus = {};
5905
+ for (const span of denied) {
5906
+ const status = readStatus(span);
5907
+ if (status === void 0) continue;
5908
+ deniedByStatus[status] = (deniedByStatus[status] ?? 0) + 1;
5909
+ }
5910
+ return {
5911
+ window: {
5912
+ lookbackMinutes: flags.lookbackMinutes ?? 60,
5913
+ from: flags.from,
5914
+ to: flags.to,
5915
+ limitPerQuery: limit
5916
+ },
5917
+ securityEvents: {
5918
+ total: events.items.length,
5919
+ bySeverity: countBy(events.items, "security.severity"),
5920
+ byCategory: countBy(events.items, "security.category"),
5921
+ byOutcome: countBy(events.items, "security.outcome"),
5922
+ topEvents: topEntries(countBy(events.items, "security.event"), 10),
5923
+ sampleTraceIds: sampleTraceIds(events.items)
5924
+ },
5925
+ suspiciousRequests: {
5926
+ total: suspicious.items.length,
5927
+ bySignal: countBy(suspicious.items, "security.signal"),
5928
+ byService: countByService(suspicious.items),
5929
+ sampleTraceIds: sampleTraceIds(suspicious.items)
5930
+ },
5931
+ deniedResponses: {
5932
+ total: denied.length,
5933
+ byStatus: deniedByStatus,
5934
+ topClients: topEntries(countBy(denied, "client.address"), 10),
5935
+ sampleTraceIds: sampleTraceIds(denied)
5936
+ }
5937
+ };
5938
+ });
5939
+ }
5940
+ async function runSecurityEvents(flags) {
5941
+ await runInvestigate("security events", flags, async (backend) => {
5942
+ const base = toSpanSearchQuery2({
5943
+ serviceName: flags.serviceName,
5944
+ lookbackMinutes: flags.lookbackMinutes,
5945
+ from: flags.from,
5946
+ to: flags.to,
5947
+ limit: flags.limit ?? DEFAULT_LIMIT
5948
+ });
5949
+ const filters = [
5950
+ { field: "security.event", operator: "exists" }
5951
+ ];
5952
+ if (flags.category !== void 0) {
5953
+ filters.push({
5954
+ field: "security.category",
5955
+ operator: "equals",
5956
+ value: flags.category
5957
+ });
5958
+ }
5959
+ if (flags.severity !== void 0) {
5960
+ filters.push({
5961
+ field: "security.severity",
5962
+ operator: "equals",
5963
+ value: flags.severity
5964
+ });
5965
+ }
5966
+ const result = await backend.searchSpans({ ...base, filters });
5967
+ return {
5968
+ totalCount: result.totalCount,
5969
+ items: result.items.map((span) => ({
5970
+ traceId: span.traceId,
5971
+ spanId: span.spanId,
5972
+ serviceName: span.serviceName,
5973
+ operationName: span.operationName,
5974
+ startTimeUnixMs: span.startTimeUnixMs,
5975
+ event: span.tags["security.event"],
5976
+ category: span.tags["security.category"],
5977
+ outcome: span.tags["security.outcome"],
5978
+ severity: span.tags["security.severity"],
5979
+ reason: span.tags["security.reason"]
5980
+ }))
5981
+ };
5982
+ });
5983
+ }
5984
+ function csvIntArg(value) {
5985
+ return value.split(",").map((part) => Number.parseInt(part.trim(), 10)).filter((n) => !Number.isNaN(n));
5986
+ }
5987
+ function registerSecurityCommands(program) {
5988
+ const securityCmd = new Command11("security").description(
5989
+ "Security telemetry: events, suspicious requests, denied responses (JSON)"
5990
+ );
5991
+ const summaryCmd = addTimeWindowFlags(new Command11("summary")).description(
5992
+ "Security posture summary: events by severity/category, probe signals, denied responses with top clients"
5993
+ ).option(
5994
+ "--denied-statuses <csv>",
5995
+ "HTTP statuses counted as denied (default 401,403,429)",
5996
+ csvIntArg
5997
+ ).action(async function() {
5998
+ const o = this.optsWithGlobals();
5999
+ await runSecuritySummary({
6000
+ ...backendFlagsFromOpts(o),
6001
+ ...windowFlagsFromOpts(o),
6002
+ deniedStatuses: o.deniedStatuses
6003
+ });
6004
+ });
6005
+ const eventsCmd = addTimeWindowFlags(new Command11("events")).description("List spans carrying security events (security.* schema)").option("--category <name>", "Filter by security.category").option("--severity <level>", "Filter by security.severity").action(async function() {
6006
+ const o = this.optsWithGlobals();
6007
+ await runSecurityEvents({
6008
+ ...backendFlagsFromOpts(o),
6009
+ ...windowFlagsFromOpts(o),
6010
+ category: o.category,
6011
+ severity: o.severity
6012
+ });
6013
+ });
6014
+ addBackendFlags(securityCmd);
6015
+ securityCmd.addCommand(summaryCmd);
6016
+ securityCmd.addCommand(eventsCmd);
6017
+ program.addCommand(securityCmd);
6018
+ }
6019
+
5785
6020
  // src/cli.ts
5786
6021
  function createProgram() {
5787
- const program = new Command11();
6022
+ const program = new Command12();
5788
6023
  program.name("autotel").description("CLI for autotel - setup wizard, diagnostics, and incremental features").version("0.1.0");
5789
6024
  const addGlobalOptions = (cmd) => {
5790
6025
  return cmd.option("--cwd <path>", "Target directory", process.cwd()).option("--verbose", "Show detailed output").option("--quiet", "Only show warnings and errors");
5791
6026
  };
5792
- const initCmd = new Command11("init").description("Initialize autotel in your project").option("--dry-run", "Skip installation and print what would be done").option("--no-install", "Generate files only, skip package installation").option("--print-install-cmd", "Output the install command without running it").option("-y, --yes", "Accept defaults, non-interactive").option("--preset <name>", "Use a quick preset (e.g., node-datadog-pino)").option("--force", "Overwrite existing config (creates backup first)").option("--workspace-root", "Install at workspace root instead of package root").option("--no-detect", "Skip auto-detection of installed deps").option("--detect-only", "Run detection, print the plan, write nothing").option("--plan <path>", "Apply a pre-built InitPlan JSON file").option("--input <path>", "Read InitPlan JSON from stdin (-) or a file").option("--scan-env", "Consent to reading .env / .env.local for backend detection").option("--json", "Emit machine-readable JSON").option("--output-file <path>", "Persist JSON output to this file").option("--no-secrets-in-output", "Redact secret-shaped values").option("--no-interactive", "Never prompt; fail if input would be required").action(async (opts) => {
6027
+ const initCmd = new Command12("init").description("Initialize autotel in your project").option("--dry-run", "Skip installation and print what would be done").option("--no-install", "Generate files only, skip package installation").option("--print-install-cmd", "Output the install command without running it").option("-y, --yes", "Accept defaults, non-interactive").option("--preset <name>", "Use a quick preset (e.g., node-datadog-pino)").option("--force", "Overwrite existing config (creates backup first)").option("--workspace-root", "Install at workspace root instead of package root").option("--no-detect", "Skip auto-detection of installed deps").option("--detect-only", "Run detection, print the plan, write nothing").option("--plan <path>", "Apply a pre-built InitPlan JSON file").option("--input <path>", "Read InitPlan JSON from stdin (-) or a file").option("--scan-env", "Consent to reading .env / .env.local for backend detection").option("--json", "Emit machine-readable JSON").option("--output-file <path>", "Persist JSON output to this file").option("--no-secrets-in-output", "Redact secret-shaped values").option("--no-interactive", "Never prompt; fail if input would be required").action(async (opts) => {
5793
6028
  const options = {
5794
6029
  cwd: opts.cwd ?? process.cwd(),
5795
6030
  dryRun: opts.dryRun ?? false,
@@ -5819,7 +6054,7 @@ function createProgram() {
5819
6054
  });
5820
6055
  addGlobalOptions(initCmd);
5821
6056
  program.addCommand(initCmd);
5822
- const doctorCmd = new Command11("doctor").description("Run diagnostics on your autotel setup").option("--json", "Output machine-readable JSON").option("--fix", "Auto-fix resolvable issues").option("--list-checks", "List all available checks").option("--env-file <path>", "Specify env file to check").action(async (opts) => {
6057
+ const doctorCmd = new Command12("doctor").description("Run diagnostics on your autotel setup").option("--json", "Output machine-readable JSON").option("--fix", "Auto-fix resolvable issues").option("--list-checks", "List all available checks").option("--env-file <path>", "Specify env file to check").action(async (opts) => {
5823
6058
  const options = {
5824
6059
  cwd: opts.cwd ?? process.cwd(),
5825
6060
  dryRun: false,
@@ -5837,7 +6072,7 @@ function createProgram() {
5837
6072
  });
5838
6073
  addGlobalOptions(doctorCmd);
5839
6074
  program.addCommand(doctorCmd);
5840
- const addCmd = new Command11("add").description("Add a backend, subscriber, plugin, or platform").argument("[type]", "Preset type (backend, subscriber, plugin, platform)").argument("[name]", "Preset name (e.g., datadog, posthog, mongoose)").option("--list", "List available presets for the given type").option("--dry-run", "Skip installation and print what would be done").option("--no-install", "Generate files only, skip package installation").option("--print-install-cmd", "Output the install command without running it").option("-y, --yes", "Accept defaults, non-interactive").option("--force", "Overwrite non-CLI-owned config (creates backup first)").option("--json", "Output machine-readable JSON (for --list)").option("--workspace-root", "Install at workspace root instead of package root").action(async (type, name, opts) => {
6075
+ const addCmd = new Command12("add").description("Add a backend, subscriber, plugin, or platform").argument("[type]", "Preset type (backend, subscriber, plugin, platform)").argument("[name]", "Preset name (e.g., datadog, posthog, mongoose)").option("--list", "List available presets for the given type").option("--dry-run", "Skip installation and print what would be done").option("--no-install", "Generate files only, skip package installation").option("--print-install-cmd", "Output the install command without running it").option("-y, --yes", "Accept defaults, non-interactive").option("--force", "Overwrite non-CLI-owned config (creates backup first)").option("--json", "Output machine-readable JSON (for --list)").option("--workspace-root", "Install at workspace root instead of package root").action(async (type, name, opts) => {
5841
6076
  const options = {
5842
6077
  cwd: opts.cwd ?? process.cwd(),
5843
6078
  dryRun: opts.dryRun ?? false,
@@ -5859,8 +6094,8 @@ function createProgram() {
5859
6094
  });
5860
6095
  addGlobalOptions(addCmd);
5861
6096
  program.addCommand(addCmd);
5862
- const codemodCmd = new Command11("codemod").description("Codemod commands for adopting autotel");
5863
- const traceCmd = new Command11("trace").description("Wrap functions in trace() with span name from function/variable/method name").argument("<path>", "File path or glob (e.g. src/index.ts, src/**/*.ts)").option("--dry-run", "Print changes without writing files").option("--name-pattern <pattern>", "Span name template: {name}, {file}, {path}").option("--skip <regex>...", "Skip functions whose name matches (repeatable)").option("--print-files", "Print per-file summary (wrapped count, skipped)").action(async (pathArg, opts) => {
6097
+ const codemodCmd = new Command12("codemod").description("Codemod commands for adopting autotel");
6098
+ const traceCmd = new Command12("trace").description("Wrap functions in trace() with span name from function/variable/method name").argument("<path>", "File path or glob (e.g. src/index.ts, src/**/*.ts)").option("--dry-run", "Print changes without writing files").option("--name-pattern <pattern>", "Span name template: {name}, {file}, {path}").option("--skip <regex>...", "Skip functions whose name matches (repeatable)").option("--print-files", "Print per-file summary (wrapped count, skipped)").action(async (pathArg, opts) => {
5864
6099
  const options = {
5865
6100
  cwd: opts.cwd ?? process.cwd(),
5866
6101
  dryRun: opts.dryRun ?? false,
@@ -5880,27 +6115,27 @@ function createProgram() {
5880
6115
  codemodCmd.addCommand(traceCmd);
5881
6116
  addGlobalOptions(codemodCmd);
5882
6117
  program.addCommand(codemodCmd);
5883
- const schemaCmd = new Command11("schema").description("Print the CLI manifest as JSON (agent discovery)").option("--output-file <path>", "Persist JSON to a file").option("--no-secrets-in-output", "Redact secret-shaped values").action((opts) => {
6118
+ const schemaCmd = new Command12("schema").description("Print the CLI manifest as JSON (agent discovery)").option("--output-file <path>", "Persist JSON to a file").option("--no-secrets-in-output", "Redact secret-shaped values").action((opts) => {
5884
6119
  runSchema({ outputFile: opts.outputFile, noSecrets: opts.secretsInOutput === false });
5885
6120
  });
5886
- const schemaErrorsCmd = new Command11("errors").description("Print error envelope shape + AUTOTEL_E_* codes").option("--output-file <path>", "Persist JSON to a file").action((opts) => {
6121
+ const schemaErrorsCmd = new Command12("errors").description("Print error envelope shape + AUTOTEL_E_* codes").option("--output-file <path>", "Persist JSON to a file").action((opts) => {
5887
6122
  runSchemaErrors({ outputFile: opts.outputFile });
5888
6123
  });
5889
- const schemaOutputsCmd = new Command11("outputs").description("Print JSON output shapes per command").option("--output-file <path>", "Persist JSON to a file").action((opts) => {
6124
+ const schemaOutputsCmd = new Command12("outputs").description("Print JSON output shapes per command").option("--output-file <path>", "Persist JSON to a file").action((opts) => {
5890
6125
  runSchemaOutputs({ outputFile: opts.outputFile });
5891
6126
  });
5892
6127
  schemaCmd.addCommand(schemaErrorsCmd);
5893
6128
  schemaCmd.addCommand(schemaOutputsCmd);
5894
6129
  program.addCommand(schemaCmd);
5895
- const commandsCmd = new Command11("commands").description("Print compact tool-style listing of commands").option("--output-file <path>", "Persist JSON to a file").action((opts) => {
6130
+ const commandsCmd = new Command12("commands").description("Print compact tool-style listing of commands").option("--output-file <path>", "Persist JSON to a file").action((opts) => {
5896
6131
  runCommandsListing({ outputFile: opts.outputFile });
5897
6132
  });
5898
6133
  program.addCommand(commandsCmd);
5899
- const examplesCmd = new Command11("examples").description("Print copy-pasteable examples for a command").argument("[command]", "Command name (omit for all)").option("--output-file <path>", "Persist JSON to a file").action((name, opts) => {
6134
+ const examplesCmd = new Command12("examples").description("Print copy-pasteable examples for a command").argument("[command]", "Command name (omit for all)").option("--output-file <path>", "Persist JSON to a file").action((name, opts) => {
5900
6135
  runExamples(name, { outputFile: opts.outputFile });
5901
6136
  });
5902
6137
  program.addCommand(examplesCmd);
5903
- const versionCmd = new Command11("version").description("Print version info as JSON").option("--output-file <path>", "Persist JSON to a file").action((opts) => {
6138
+ const versionCmd = new Command12("version").description("Print version info as JSON").option("--output-file <path>", "Persist JSON to a file").action((opts) => {
5904
6139
  runVersion({ outputFile: opts.outputFile });
5905
6140
  });
5906
6141
  program.addCommand(versionCmd);
@@ -5915,6 +6150,7 @@ function createProgram() {
5915
6150
  registerSemconvCommands(program);
5916
6151
  registerScoreCommands(program);
5917
6152
  registerCollectorCommands(program);
6153
+ registerSecurityCommands(program);
5918
6154
  return program;
5919
6155
  }
5920
6156
  async function run() {
@@ -5966,7 +6202,7 @@ function jsonModeRequested() {
5966
6202
  run().catch((error2) => {
5967
6203
  const err = commanderErrorToAutotel(error2) ?? toAutotelError(error2);
5968
6204
  const isJson = jsonModeRequested() || // schema/commands/examples/version + investigate commands are JSON-only
5969
- /^(schema|commands|examples|version|health|capabilities|discover|query|trace|diagnose|topology|correlate|llm|semconv|score|collector)\b/.test(
6205
+ /^(schema|commands|examples|version|health|capabilities|discover|query|trace|diagnose|topology|correlate|llm|semconv|score|collector|security)\b/.test(
5970
6206
  process.argv.slice(2).join(" ")
5971
6207
  );
5972
6208
  if (isJson) {