comber 0.1.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.
Files changed (158) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +732 -0
  3. package/action.yml +279 -0
  4. package/comber.config.example.json +64 -0
  5. package/dist/api/classify.js +99 -0
  6. package/dist/api/classify.js.map +1 -0
  7. package/dist/api/driver.js +235 -0
  8. package/dist/api/driver.js.map +1 -0
  9. package/dist/api/graphql-driver.js +150 -0
  10. package/dist/api/graphql-driver.js.map +1 -0
  11. package/dist/api/graphql.js +115 -0
  12. package/dist/api/graphql.js.map +1 -0
  13. package/dist/api/har-driver.js +167 -0
  14. package/dist/api/har-driver.js.map +1 -0
  15. package/dist/api/har.js +100 -0
  16. package/dist/api/har.js.map +1 -0
  17. package/dist/api/load.js +83 -0
  18. package/dist/api/load.js.map +1 -0
  19. package/dist/api/openapi.js +159 -0
  20. package/dist/api/openapi.js.map +1 -0
  21. package/dist/api/request.js +141 -0
  22. package/dist/api/request.js.map +1 -0
  23. package/dist/api/schema.js +126 -0
  24. package/dist/api/schema.js.map +1 -0
  25. package/dist/cli.js +352 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/comment.js +164 -0
  28. package/dist/comment.js.map +1 -0
  29. package/dist/config.js +664 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/connectors/builtin/discord.js +55 -0
  32. package/dist/connectors/builtin/github-checks.js +53 -0
  33. package/dist/connectors/builtin/junit.js +70 -0
  34. package/dist/connectors/builtin/linear.js +58 -0
  35. package/dist/connectors/builtin/slack.js +60 -0
  36. package/dist/connectors/builtin/webhook.js +39 -0
  37. package/dist/connectors/index.js +3 -0
  38. package/dist/connectors/loader.js +72 -0
  39. package/dist/connectors/registry.js +28 -0
  40. package/dist/connectors/types.js +23 -0
  41. package/dist/core/action-gate.js +48 -0
  42. package/dist/core/baseline.js +85 -0
  43. package/dist/core/baseline.js.map +1 -0
  44. package/dist/core/blind-spots.js +22 -0
  45. package/dist/core/blind-spots.js.map +1 -0
  46. package/dist/core/contract-extras.js +1 -0
  47. package/dist/core/contract-extras.js.map +1 -0
  48. package/dist/core/contract.js +1 -0
  49. package/dist/core/contract.js.map +1 -0
  50. package/dist/core/coverage.js +70 -0
  51. package/dist/core/coverage.js.map +1 -0
  52. package/dist/core/crawl.js +229 -0
  53. package/dist/core/crawl.js.map +1 -0
  54. package/dist/core/egress.js +128 -0
  55. package/dist/core/egress.js.map +1 -0
  56. package/dist/core/ledger.js +41 -0
  57. package/dist/core/ledger.js.map +1 -0
  58. package/dist/core/net.js +39 -0
  59. package/dist/core/pipeline.js +191 -0
  60. package/dist/core/pipeline.js.map +1 -0
  61. package/dist/core/plan.js +121 -0
  62. package/dist/core/plan.js.map +1 -0
  63. package/dist/core/redact.js +240 -0
  64. package/dist/core/redact.js.map +1 -0
  65. package/dist/core/replay.js +263 -0
  66. package/dist/core/replay.js.map +1 -0
  67. package/dist/core/report.js +253 -0
  68. package/dist/core/report.js.map +1 -0
  69. package/dist/core/safety-judge.js +116 -0
  70. package/dist/core/safety.js +92 -0
  71. package/dist/core/safety.js.map +1 -0
  72. package/dist/core/signal-normalize.js +190 -0
  73. package/dist/core/signal-normalize.js.map +1 -0
  74. package/dist/core/signals.js +5 -0
  75. package/dist/core/signals.js.map +1 -0
  76. package/dist/core/triage.js +104 -0
  77. package/dist/core/triage.js.map +1 -0
  78. package/dist/core/types.js +1 -0
  79. package/dist/core/types.js.map +1 -0
  80. package/dist/desktop/desktop-profile.js +6 -0
  81. package/dist/desktop/desktop-profile.js.map +1 -0
  82. package/dist/desktop/driver.js +433 -0
  83. package/dist/desktop/driver.js.map +1 -0
  84. package/dist/desktop/evidence.js +206 -0
  85. package/dist/desktop/evidence.js.map +1 -0
  86. package/dist/desktop/macos/accessibility-runner.js +118 -0
  87. package/dist/desktop/macos/accessibility-runner.js.map +1 -0
  88. package/dist/desktop/macos/types.js +1 -0
  89. package/dist/desktop/macos/types.js.map +1 -0
  90. package/dist/desktop/preflight.js +107 -0
  91. package/dist/desktop/preflight.js.map +1 -0
  92. package/dist/desktop/proxy-proof.js +185 -0
  93. package/dist/desktop/proxy-proof.js.map +1 -0
  94. package/dist/desktop/view.js +348 -0
  95. package/dist/desktop/view.js.map +1 -0
  96. package/dist/desktop/windows/types.js +1 -0
  97. package/dist/desktop/windows/types.js.map +1 -0
  98. package/dist/desktop/windows/uia-runner.js +127 -0
  99. package/dist/desktop/windows/uia-runner.js.map +1 -0
  100. package/dist/native/apple.js +14 -0
  101. package/dist/native/apple.js.map +1 -0
  102. package/dist/native/device-profile.js +6 -0
  103. package/dist/native/device-profile.js.map +1 -0
  104. package/dist/native/driver.js +416 -0
  105. package/dist/native/driver.js.map +1 -0
  106. package/dist/native/evidence.js +164 -0
  107. package/dist/native/evidence.js.map +1 -0
  108. package/dist/native/load.js +9 -0
  109. package/dist/native/load.js.map +1 -0
  110. package/dist/native/maestro.js +153 -0
  111. package/dist/native/maestro.js.map +1 -0
  112. package/dist/native/preflight.js +72 -0
  113. package/dist/native/preflight.js.map +1 -0
  114. package/dist/native/proxy-proof.js +182 -0
  115. package/dist/native/proxy-proof.js.map +1 -0
  116. package/dist/native/samsung.js +34 -0
  117. package/dist/native/samsung.js.map +1 -0
  118. package/dist/native/signals.js +50 -0
  119. package/dist/native/signals.js.map +1 -0
  120. package/dist/native/view.js +139 -0
  121. package/dist/native/view.js.map +1 -0
  122. package/dist/profile.js +237 -0
  123. package/dist/profile.js.map +1 -0
  124. package/dist/sink.js +134 -0
  125. package/dist/sink.js.map +1 -0
  126. package/dist/sinks/file.js +49 -0
  127. package/dist/sinks/file.js.map +1 -0
  128. package/dist/sinks/http.js +38 -0
  129. package/dist/sinks/http.js.map +1 -0
  130. package/dist/sinks/index.js +5 -0
  131. package/dist/sinks/index.js.map +1 -0
  132. package/dist/sinks/multi.js +40 -0
  133. package/dist/sinks/multi.js.map +1 -0
  134. package/dist/sinks/paths.js +32 -0
  135. package/dist/sinks/paths.js.map +1 -0
  136. package/dist/sinks/record.js +58 -0
  137. package/dist/sinks/record.js.map +1 -0
  138. package/dist/sinks/types.js +1 -0
  139. package/dist/sinks/types.js.map +1 -0
  140. package/dist/web/breakage.js +210 -0
  141. package/dist/web/breakage.js.map +1 -0
  142. package/dist/web/controls.js +102 -0
  143. package/dist/web/controls.js.map +1 -0
  144. package/dist/web/driver.js +803 -0
  145. package/dist/web/driver.js.map +1 -0
  146. package/dist/web/elements.js +151 -0
  147. package/dist/web/elements.js.map +1 -0
  148. package/dist/web/fingerprint.js +13 -0
  149. package/dist/web/fingerprint.js.map +1 -0
  150. package/dist/web/forms.js +74 -0
  151. package/dist/web/forms.js.map +1 -0
  152. package/dist/web/links.js +194 -0
  153. package/dist/web/links.js.map +1 -0
  154. package/dist/web/types.js +1 -0
  155. package/dist/web/types.js.map +1 -0
  156. package/package.json +80 -0
  157. package/runtime/macos-accessibility-runner.js +275 -0
  158. package/runtime/windows-uia-runner.ps1 +539 -0
@@ -0,0 +1,141 @@
1
+ import { operationLabel } from "./openapi.js";
2
+ const MAX_DEPTH = 6;
3
+ function firstExampleValue(examples) {
4
+ if (!examples)
5
+ return undefined;
6
+ for (const ex of Object.values(examples))
7
+ if (ex && "value" in ex)
8
+ return ex.value;
9
+ return undefined;
10
+ }
11
+ function sampleString(schema) {
12
+ switch (schema.format) {
13
+ case "uuid":
14
+ return "00000000-0000-0000-0000-000000000000";
15
+ case "date":
16
+ return "2020-01-01";
17
+ case "date-time":
18
+ return "2020-01-01T00:00:00Z";
19
+ case "email":
20
+ return "sample@example.com";
21
+ case "uri":
22
+ case "url":
23
+ return "https://example.com";
24
+ case "hostname":
25
+ return "example.com";
26
+ case "ipv4":
27
+ return "127.0.0.1";
28
+ case "byte":
29
+ return "c2FtcGxl"; // base64("sample")
30
+ default:
31
+ return "string";
32
+ }
33
+ }
34
+ /** A deterministic, minimal value satisfying `schema`. example/default/enum win first. */
35
+ export function sampleValue(schema, resolver, depth = 0) {
36
+ const s = resolver.deref(schema);
37
+ if (!s || typeof s !== "object")
38
+ return "string";
39
+ if (s.example !== undefined)
40
+ return s.example;
41
+ if (s.default !== undefined)
42
+ return s.default;
43
+ if (Array.isArray(s.enum) && s.enum.length > 0)
44
+ return s.enum[0];
45
+ const type = Array.isArray(s.type) ? s.type[0] : s.type;
46
+ if (type === "object" || (!type && s.properties)) {
47
+ if (depth >= MAX_DEPTH)
48
+ return {};
49
+ const out = {};
50
+ for (const [key, prop] of Object.entries(s.properties ?? {}))
51
+ out[key] = sampleValue(prop, resolver, depth + 1);
52
+ return out;
53
+ }
54
+ switch (type) {
55
+ case "integer":
56
+ case "number":
57
+ return 1;
58
+ case "boolean":
59
+ return true;
60
+ case "array":
61
+ return depth >= MAX_DEPTH ? [] : [sampleValue(s.items, resolver, depth + 1)];
62
+ case "string":
63
+ return sampleString(s);
64
+ default:
65
+ return "string";
66
+ }
67
+ }
68
+ function paramValue(p, resolver) {
69
+ if (p.example !== undefined)
70
+ return p.example;
71
+ const ex = firstExampleValue(p.examples);
72
+ if (ex !== undefined)
73
+ return ex;
74
+ return sampleValue(p.schema, resolver);
75
+ }
76
+ function scalarString(v) {
77
+ return v !== null && typeof v === "object" ? JSON.stringify(v) : String(v);
78
+ }
79
+ function pickJsonMedia(content) {
80
+ return (content["application/json"] ??
81
+ Object.entries(content).find(([mt]) => /\bjson\b/i.test(mt))?.[1] ??
82
+ Object.values(content)[0]);
83
+ }
84
+ function joinUrl(base, path) {
85
+ return `${base.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
86
+ }
87
+ /** Build the concrete request for an operation (seed → spec example → schema sample). */
88
+ export function buildRequest(base, op, resolver, seed) {
89
+ const seedEntry = seed?.[operationLabel(op)] ?? {};
90
+ let path = op.path;
91
+ for (const p of op.parameters) {
92
+ if (p.in !== "path")
93
+ continue;
94
+ const v = seedEntry.path?.[p.name] ?? paramValue(p, resolver);
95
+ path = path.replace(`{${p.name}}`, encodeURIComponent(scalarString(v)));
96
+ }
97
+ const qs = new URLSearchParams();
98
+ for (const p of op.parameters) {
99
+ if (p.in !== "query")
100
+ continue;
101
+ const seeded = seedEntry.query?.[p.name];
102
+ const schema = resolver.deref(p.schema);
103
+ const hasDefault = schema?.default !== undefined || p.example !== undefined;
104
+ if (seeded === undefined && !p.required && !hasDefault)
105
+ continue; // keep the request minimal
106
+ qs.set(p.name, scalarString(seeded ?? paramValue(p, resolver)));
107
+ }
108
+ const headers = {};
109
+ for (const p of op.parameters) {
110
+ if (p.in !== "header")
111
+ continue;
112
+ const seeded = seedEntry.headers?.[p.name];
113
+ if (seeded !== undefined)
114
+ headers[p.name] = String(seeded);
115
+ else if (p.required)
116
+ headers[p.name] = scalarString(paramValue(p, resolver));
117
+ }
118
+ let body;
119
+ let bodyContentType;
120
+ if (op.requestBody) {
121
+ const media = pickJsonMedia(op.requestBody.content);
122
+ if (media) {
123
+ const value = seedEntry.body !== undefined
124
+ ? seedEntry.body
125
+ : media.example !== undefined
126
+ ? media.example
127
+ : (firstExampleValue(media.examples) ?? sampleValue(media.schema, resolver));
128
+ body = JSON.stringify(value);
129
+ bodyContentType = "application/json";
130
+ }
131
+ }
132
+ const query = qs.toString();
133
+ return {
134
+ method: op.method,
135
+ url: joinUrl(base, path) + (query ? `?${query}` : ""),
136
+ pathTemplate: op.path,
137
+ headers,
138
+ body,
139
+ bodyContentType,
140
+ };
141
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../../src/api/request.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,126 @@
1
+ // Full JSON-Schema response-body validation (ajv) — the "credible" upgrade to the API
2
+ // driver's light required-property check. Given an OpenAPI response schema (which may carry
3
+ // `$ref`s into `components`), we resolve the refs into a self-contained schema, compile it
4
+ // once with ajv (+ ajv-formats), and validate the actual response body against it. Failures
5
+ // become `response-schema-violation` carrying the ajv error PATHS only (never body values),
6
+ // truncated, so the finding is redaction-safe.
7
+ //
8
+ // Leaf module of the api package: imports the new `ajv`/`ajv-formats` libs + the sibling
9
+ // openapi types ONLY (never `core`, never `web`). The dependency arrow stays api → libs.
10
+ import { Ajv } from "ajv";
11
+ import * as ajvFormats from "ajv-formats";
12
+ // ajv-formats ships as CJS with a default export; under NodeNext the resolved value (default,
13
+ // or the module itself depending on interop) is the callable plugin. Pin it once, defensively.
14
+ const addFormats = (ajvFormats.default ?? ajvFormats);
15
+ const MAX_VIOLATIONS = 8;
16
+ const MAX_PATH_LEN = 80;
17
+ const MAX_MSG_LEN = 120;
18
+ const MAX_RESOLVE_DEPTH = 64;
19
+ /**
20
+ * Deep-resolve every `$ref` in a schema into a self-contained JSON Schema ajv can compile
21
+ * without knowing OpenAPI pointer semantics. Cyclic schemas (a `Node` whose children are
22
+ * `Node`s) are broken by replacing a ref already open on the current path with `{}` (accept
23
+ * anything) — so validation terminates and never over-rejects a legitimately recursive body.
24
+ * OpenAPI 3.0 `nullable: true` is translated to a JSON-Schema `"null"` type union.
25
+ */
26
+ export function resolveSchema(schema, resolver) {
27
+ const walk = (node, active, depth) => {
28
+ if (depth > MAX_RESOLVE_DEPTH)
29
+ return {};
30
+ if (Array.isArray(node))
31
+ return node.map((n) => walk(n, active, depth + 1));
32
+ if (!node || typeof node !== "object")
33
+ return node;
34
+ const ref = node.$ref;
35
+ if (typeof ref === "string") {
36
+ if (active.has(ref))
37
+ return {}; // cycle → permissive, terminates
38
+ const target = resolver.deref(node);
39
+ const next = new Set(active);
40
+ next.add(ref);
41
+ return walk(target, next, depth + 1);
42
+ }
43
+ const out = {};
44
+ for (const [k, v] of Object.entries(node)) {
45
+ out[k] = walk(v, active, depth + 1);
46
+ }
47
+ // OpenAPI 3.0 nullable → JSON-Schema null union (3.1 already uses `type: [...]`).
48
+ if (out.nullable === true && typeof out.type === "string") {
49
+ out.type = [out.type, "null"];
50
+ }
51
+ delete out.nullable;
52
+ return out;
53
+ };
54
+ return walk(schema, new Set(), 0);
55
+ }
56
+ const truncate = (s, n) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);
57
+ /** One ajv instance per validator set: tolerant of OpenAPI-isms (unknown keywords/formats are
58
+ * ignored, not fatal) so a real-world spec never crashes the checker. */
59
+ function makeAjv() {
60
+ // `logger: false` keeps ajv silent — a checker that prints unknown-keyword warnings to the
61
+ // console for every real-world spec would be noisy in CI. `strict: false` tolerates OpenAPI-isms.
62
+ const ajv = new Ajv({ allErrors: true, strict: false, validateFormats: true, allowUnionTypes: true, logger: false });
63
+ addFormats(ajv);
64
+ return ajv;
65
+ }
66
+ /**
67
+ * Compiles + caches response schemas and validates bodies against them. One instance is held
68
+ * per driver (per spec) so the ajv compile cost is paid once per distinct schema, keyed by the
69
+ * ORIGINAL schema object identity (the resolver/doc are stable for a run).
70
+ */
71
+ export class ResponseValidator {
72
+ resolver;
73
+ ajv = makeAjv();
74
+ cache = new WeakMap();
75
+ constructor(resolver) {
76
+ this.resolver = resolver;
77
+ }
78
+ /** Compile (once) the self-contained form of `schema`; `null` if it cannot compile. */
79
+ compile(schema) {
80
+ if (typeof schema !== "object" || schema === null)
81
+ return null;
82
+ const cached = this.cache.get(schema);
83
+ if (cached !== undefined)
84
+ return cached;
85
+ let fn = null;
86
+ try {
87
+ fn = this.ajv.compile(resolveSchema(schema, this.resolver));
88
+ }
89
+ catch {
90
+ fn = null; // malformed schema → caller falls back to the light check
91
+ }
92
+ this.cache.set(schema, fn);
93
+ return fn;
94
+ }
95
+ /**
96
+ * Validate a JSON response body against `schema`. Returns `validated:false` (so the caller
97
+ * falls back) when the schema won't compile OR the body is not JSON — neither is a schema
98
+ * violation by itself (a non-JSON body is a content-type concern, handled elsewhere).
99
+ */
100
+ validate(schema, bodyText) {
101
+ const fn = this.compile(schema);
102
+ if (!fn)
103
+ return { validated: false, ok: false, violations: [] };
104
+ let body;
105
+ try {
106
+ body = JSON.parse(bodyText);
107
+ }
108
+ catch {
109
+ return { validated: false, ok: false, violations: [] };
110
+ }
111
+ const ok = fn(body);
112
+ if (ok)
113
+ return { validated: true, ok: true, violations: [] };
114
+ const errs = fn.errors ?? [];
115
+ const violations = errs.slice(0, MAX_VIOLATIONS).map((e) => ({
116
+ path: truncate(e.instancePath || "(root)", MAX_PATH_LEN),
117
+ message: truncate(e.message ?? "is invalid", MAX_MSG_LEN),
118
+ }));
119
+ return { validated: true, ok: false, violations };
120
+ }
121
+ }
122
+ /** A compact, stable, redaction-safe one-liner summarizing the violations for a finding
123
+ * message — paths + schema messages only, never the body's values. */
124
+ export function formatViolations(violations) {
125
+ return violations.map((v) => `${v.path} ${v.message}`).join("; ");
126
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/api/schema.ts"],"names":[],"mappings":"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"}
package/dist/cli.js ADDED
@@ -0,0 +1,352 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { readFileSync } from "node:fs";
4
+ import { randomUUID } from "node:crypto";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { loadConfig } from "./config.js";
8
+ import { WebDriver } from "./web/driver.js";
9
+ import { ApiDriver } from "./api/driver.js";
10
+ import { GraphqlDriver } from "./api/graphql-driver.js";
11
+ import { HarDriver } from "./api/har-driver.js";
12
+ import { loadHar, loadOpenApiDocument, loadRoleSeedData, loadSeedData } from "./api/load.js";
13
+ import { NativeDriver } from "./native/driver.js";
14
+ import { buildDefaultRunner } from "./native/load.js";
15
+ import { DesktopDriver } from "./desktop/driver.js";
16
+ import { DefaultMacosAccessibilityRunner } from "./desktop/macos/accessibility-runner.js";
17
+ import { DefaultWindowsUiaRunner } from "./desktop/windows/uia-runner.js";
18
+ import { crawl } from "./core/crawl.js";
19
+ import { replay } from "./core/replay.js";
20
+ import { loadPlan } from "./core/plan.js";
21
+ import { triage } from "./core/triage.js";
22
+ import { writeReport } from "./core/report.js";
23
+ import { MultiSink } from "./sinks/index.js";
24
+ import { ConnectorFanout, builtinManifests } from "./connectors/index.js";
25
+ import { loadProfileFromArgv } from "./profile.js";
26
+ import { compareBaselineFromPath } from "./core/baseline.js";
27
+ import { writePrComment } from "./comment.js";
28
+ import { redactResult, redactUrl } from "./core/redact.js";
29
+ /** Resolve the package version from the shipped package.json (dist/cli.js → ../package.json). */
30
+ function packageVersion() {
31
+ try {
32
+ const here = dirname(fileURLToPath(import.meta.url));
33
+ const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"));
34
+ return pkg.version ?? "0.0.0";
35
+ }
36
+ catch {
37
+ return "0.0.0";
38
+ }
39
+ }
40
+ const HELP = `🦞 Comber — every-click AI QA agent. Walks every inch of an app and reports what washed up.
41
+
42
+ USAGE
43
+ comber <url> [options] crawl a web app / site / SaaS surface
44
+ comber api --openapi <path|url> --api-base <url> [options]
45
+ comber api --graphql <url> [options] introspect + exercise a GraphQL endpoint
46
+ comber api --har <path> [options] replay a recorded HAR and check for drift
47
+ comber native --app <id|apk|path> [--platform android|ios] [--device ID] [--deeplink URI]...
48
+ comber desktop --platform windows|macos (--app-path PATH | --process-name NAME | --app-id ID)
49
+
50
+ COMMON OPTIONS
51
+ --profile NAME use a named profile from comber.config.json
52
+ --config PATH path to the config file (default: ./comber.config.json)
53
+ --strict exit 1 when any 'fail' finding is present (gate CI)
54
+ --llm / --no-llm enable/disable the Opus finding-judge (needs ANTHROPIC_API_KEY)
55
+ --headed run the browser headed (web)
56
+ --allow-writes exercise mutating controls/methods (DANGER: off by default)
57
+ --cross-origin follow links off the start origin (default: same-origin only)
58
+ --external-links verify external links (HEAD/GET)
59
+ --no-screenshots skip per-state screenshots (raw pixels of the target; sensitive)
60
+ --baseline PATH diff findings against a prior baseline
61
+ --pr-comment PATH write a Markdown PR-comment summary
62
+ --replay PATH deterministic re-run of a recorded plan.json (no discovery, no LLM)
63
+ --out DIR output directory (default: runs)
64
+
65
+ CAPS (runaway-cost backstops)
66
+ --max-states N --max-actions N --max-ms N
67
+ --max-per-state N --max-visits-per-route N --max-tokens N --near-dup 0..1
68
+
69
+ API OPTIONS
70
+ --allow-private-target target a private/staging base you own (bypass the SSRF egress guard)
71
+ --api-timeout MS per-request deadline for API fetches (default 15000)
72
+
73
+ Full docs: https://codecomber.io · exit codes: 0 clean · 1 --strict+fails · 2 error`;
74
+ /**
75
+ * Driver-selection seam (the composition root). The core engine is target-agnostic; a target
76
+ * is just a Driver. `target: "api"` loads an OpenAPI spec + optional seed and builds the
77
+ * ApiDriver; `target: "native"` builds the Maestro-backed NativeDriver (default shell-out runner);
78
+ * everything else keeps the existing WebDriver path (and all of its flags) intact.
79
+ */
80
+ async function buildDriver(cfg) {
81
+ if (cfg.target === "api") {
82
+ const guard = { allowPrivate: cfg.apiAllowPrivateTarget, timeoutMs: cfg.apiTimeoutMs };
83
+ if (cfg.apiSourceKind === "graphql") {
84
+ // The GraphqlDriver introspects the endpoint over its (guarded fetch) transport at start().
85
+ return new GraphqlDriver({
86
+ endpoint: cfg.graphqlUrl,
87
+ allowWrites: !cfg.blockWrites,
88
+ token: cfg.apiToken,
89
+ allowPrivateTarget: cfg.apiAllowPrivateTarget,
90
+ timeoutMs: cfg.apiTimeoutMs,
91
+ });
92
+ }
93
+ if (cfg.apiSourceKind === "har") {
94
+ const har = await loadHar(cfg.harPath);
95
+ return new HarDriver({
96
+ har,
97
+ allowWrites: !cfg.blockWrites,
98
+ token: cfg.apiToken,
99
+ allowPrivateTarget: cfg.apiAllowPrivateTarget,
100
+ timeoutMs: cfg.apiTimeoutMs,
101
+ });
102
+ }
103
+ // OpenAPI (JSON or YAML — the loader content-sniffs). The URL fetch is egress-guarded too.
104
+ const doc = await loadOpenApiDocument(cfg.openapiSource, guard);
105
+ const seed = await loadSeedData(cfg.apiSeedDataPath);
106
+ const roleSeed = await loadRoleSeedData(cfg.apiRoleSeedPath);
107
+ return new ApiDriver({
108
+ doc,
109
+ base: cfg.apiBase,
110
+ allowWrites: !cfg.blockWrites,
111
+ token: cfg.apiToken,
112
+ seed,
113
+ authProfiles: cfg.apiAuthProfiles,
114
+ ownerScoped: cfg.apiOwnerScoped ? new Set(cfg.apiOwnerScoped) : undefined,
115
+ roleSeed,
116
+ allowPrivateTarget: cfg.apiAllowPrivateTarget,
117
+ timeoutMs: cfg.apiTimeoutMs,
118
+ });
119
+ }
120
+ if (cfg.target === "native") {
121
+ const runner = buildDefaultRunner({
122
+ appId: cfg.nativeApp,
123
+ platform: cfg.nativePlatform ?? "android",
124
+ device: cfg.nativeDevice,
125
+ });
126
+ return new NativeDriver({
127
+ appId: cfg.nativeApp,
128
+ platform: cfg.nativePlatform ?? "android",
129
+ deeplinks: cfg.nativeDeeplinks,
130
+ runner,
131
+ deviceProfile: cfg.nativeDeviceProfile,
132
+ });
133
+ }
134
+ if (cfg.target === "desktop") {
135
+ if (cfg.desktopPlatform === "macos") {
136
+ if (cfg.desktopAppProfile?.automationBackend === "xctest") {
137
+ throw new Error("desktop macOS XCTest backend is not implemented; use --automation-backend accessibility");
138
+ }
139
+ const runner = new DefaultMacosAccessibilityRunner({
140
+ appId: cfg.desktopAppId,
141
+ appPath: cfg.desktopAppPath,
142
+ processName: cfg.desktopProcessName,
143
+ });
144
+ return new DesktopDriver({
145
+ platform: "macos",
146
+ appId: cfg.desktopAppId,
147
+ appPath: cfg.desktopAppPath,
148
+ processName: cfg.desktopProcessName,
149
+ runner,
150
+ desktopProfile: cfg.desktopAppProfile,
151
+ });
152
+ }
153
+ const runner = new DefaultWindowsUiaRunner({
154
+ appId: cfg.desktopAppId,
155
+ appPath: cfg.desktopAppPath,
156
+ processName: cfg.desktopProcessName,
157
+ processId: cfg.desktopProcessId,
158
+ windowTitle: cfg.desktopWindowTitle,
159
+ });
160
+ return new DesktopDriver({
161
+ platform: "windows",
162
+ appId: cfg.desktopAppId,
163
+ appPath: cfg.desktopAppPath,
164
+ processName: cfg.desktopProcessName,
165
+ processId: cfg.desktopProcessId,
166
+ windowTitle: cfg.desktopWindowTitle,
167
+ runner,
168
+ desktopProfile: cfg.desktopAppProfile,
169
+ });
170
+ }
171
+ return new WebDriver({
172
+ headless: cfg.headless,
173
+ storageStatePath: cfg.storageStatePath,
174
+ blockWrites: cfg.blockWrites,
175
+ egressUrlDenylist: cfg.egressUrlDenylist,
176
+ startUrl: cfg.startUrl,
177
+ sameOriginOnly: cfg.sameOriginOnly,
178
+ allowedOrigins: cfg.allowedOrigins,
179
+ denylist: cfg.denylist,
180
+ destructiveHrefDenylist: cfg.destructiveHrefDenylist,
181
+ checkExternalLinks: cfg.checkExternalLinks,
182
+ maxExternalLinks: cfg.maxExternalLinks,
183
+ linkTimeoutMs: cfg.linkTimeoutMs,
184
+ linkConcurrency: cfg.linkConcurrency,
185
+ });
186
+ }
187
+ async function main() {
188
+ const argv = process.argv.slice(2);
189
+ // Help/version are the first thing any dev or CI probe tries — they must print and exit 0,
190
+ // never fall into loadConfig's "usage" throw (which the crash handler re-exits as code 2).
191
+ if (argv.includes("--help") || argv.includes("-h")) {
192
+ console.log(HELP);
193
+ process.exit(0);
194
+ }
195
+ if (argv.includes("--version") || argv.includes("-V")) {
196
+ console.log(packageVersion());
197
+ process.exit(0);
198
+ }
199
+ // `comber connectors` — list the built-in connectors (the marketplace catalog's first-party set).
200
+ if (argv[0] === "connectors") {
201
+ console.log("🦞 Comber connectors (built-in):\n");
202
+ for (const m of builtinManifests()) {
203
+ const env = m.env?.length ? ` env: ${m.env.join(", ")}` : "";
204
+ console.log(` ${m.name.padEnd(10)} ${m.kind.padEnd(13)} ${m.summary}`);
205
+ if (env)
206
+ console.log(` ${" ".repeat(10)} ${env.trim()}`);
207
+ }
208
+ console.log(`\nUse in comber.config.json: "connectors": [{ "use": "slack", "on": "fail" }]\n` +
209
+ `Install a marketplace connector, then reference its package name in "use".\n` +
210
+ `Authoring guide: https://codecomber.io/connectors`);
211
+ process.exit(0);
212
+ }
213
+ const loadedProfile = await loadProfileFromArgv(argv);
214
+ const cfg = loadConfig(argv, loadedProfile.profile);
215
+ const strict = argv.includes("--strict");
216
+ const runDir = join(cfg.outDir, `run-${Date.now()}`);
217
+ await mkdir(runDir, { recursive: true });
218
+ const mode = cfg.replayPlanPath
219
+ ? "replaying"
220
+ : cfg.target === "api"
221
+ ? "checking API"
222
+ : cfg.target === "native"
223
+ ? "checking native app"
224
+ : cfg.target === "desktop"
225
+ ? "checking desktop app"
226
+ : "crawling";
227
+ // Redact the target in the banner too — a start URL with `?token=`/`#access_token=`/a
228
+ // signed-URL param must not land in stdout/CI logs (PRODUCT.md: "logs never carry secrets").
229
+ console.log(`🦞 Comber ${mode} ${redactUrl(cfg.startUrl)}`);
230
+ if (cfg.replayPlanPath)
231
+ console.log(` replay: deterministic re-run of ${cfg.replayPlanPath} (no discovery, no LLM)`);
232
+ console.log(` mode: ${cfg.useLLM ? "heuristic action-picker + LLM finding-judge (Opus)" : "heuristic (no ANTHROPIC_API_KEY)"}` +
233
+ ` caps: ${cfg.maxStates} states / ${cfg.maxActions} actions / ${(cfg.maxWallClockMs / 1000) | 0}s`);
234
+ if (cfg.target === "api") {
235
+ const src = redactUrl(cfg.graphqlUrl ?? cfg.harPath ?? cfg.openapiSource ?? "");
236
+ console.log(` source: ${cfg.apiSourceKind} ${src} writes: ${cfg.blockWrites ? "read-only" : "ALLOWED (--allow-writes)"}` +
237
+ (cfg.apiAuthProfiles?.length ? ` roles: ${cfg.apiAuthProfiles.map((p) => p.role).join(", ")}` : ""));
238
+ }
239
+ if (cfg.target === "native") {
240
+ console.log(` app: ${cfg.nativeApp} (${cfg.nativePlatform ?? "android"})` +
241
+ `${cfg.nativeDevice ? ` device: ${cfg.nativeDevice}` : ""}` +
242
+ ` deeplinks: ${cfg.nativeDeeplinks?.length ?? 0}` +
243
+ ` ⚠️ network-egress safety NOT enforced (no Maestro request interception — see README)`);
244
+ }
245
+ if (cfg.target === "desktop") {
246
+ const backend = cfg.desktopAppProfile?.automationBackend ?? (cfg.desktopPlatform === "macos" ? "accessibility" : "uia");
247
+ const desktopIdentity = cfg.desktopAppPath ?? cfg.desktopProcessName ?? cfg.desktopAppId ?? `pid:${cfg.desktopProcessId}`;
248
+ console.log(` app: ${desktopIdentity} (${cfg.desktopPlatform ?? "windows"} ${backend})` +
249
+ ` ⚠️ network-egress safety NOT enforced without verified OS-level proxy evidence`);
250
+ }
251
+ if (cfg.profileName) {
252
+ console.log(` profile: ${cfg.profileName}${loadedProfile.configPath ? ` (${loadedProfile.configPath})` : ""}`);
253
+ }
254
+ const driver = await buildDriver(cfg);
255
+ const result = cfg.replayPlanPath
256
+ ? await replay(driver, cfg, runDir, await loadPlan(cfg.replayPlanPath))
257
+ : await crawl(driver, cfg, runDir);
258
+ const judged = await triage(result.findings, {
259
+ useLLM: cfg.useLLM,
260
+ anthropicKey: cfg.anthropicKey,
261
+ baseURL: cfg.hostedInferenceUrl,
262
+ });
263
+ result.findings = judged.findings;
264
+ result.tokenSpend.input += judged.spend.input;
265
+ result.tokenSpend.output += judged.spend.output;
266
+ if (cfg.baselinePath) {
267
+ result.baseline = await compareBaselineFromPath(result, cfg.baselinePath);
268
+ }
269
+ // Redact ONCE before any artifact write. finding.url/message + startUrl +
270
+ // visited/link URLs routinely carry crawled-app secrets (OAuth #access_token,
271
+ // ?token= magic links, signed-URL params, path tokens); the report.html and
272
+ // result.json artifacts are uploaded to CI and the PR comment is posted to a
273
+ // (possibly public) PR, so none of those surfaces may see raw findings.
274
+ // (The sink redacts internally, so it still gets the raw result.)
275
+ const redacted = redactResult(result);
276
+ const reportPath = await writeReport(redacted, runDir, new Date().toISOString());
277
+ if (cfg.prCommentPath) {
278
+ const commentPath = await writePrComment(redacted, reportPath, cfg.prCommentPath);
279
+ console.log(` pr-comment: ${commentPath}`);
280
+ }
281
+ await writeFile(join(runDir, "result.json"), JSON.stringify(redacted, null, 2)).catch(() => { });
282
+ const runMeta = {
283
+ runId: randomUUID(),
284
+ app: (() => {
285
+ try {
286
+ return new URL(cfg.startUrl).hostname;
287
+ }
288
+ catch {
289
+ return undefined;
290
+ }
291
+ })(),
292
+ mode: cfg.useLLM ? "llm" : "heuristic",
293
+ runDir,
294
+ };
295
+ // Persistence — opt-in, no-op when unconfigured, never breaks a run.
296
+ const sink = MultiSink.fromEnv();
297
+ if (sink.active.length) {
298
+ await sink.recordRun(result, runMeta);
299
+ console.log(` sinks: ${sink.active.join(", ")}`);
300
+ }
301
+ // Connectors — deliver the finished run where developers work (Slack/Discord/GitHub/Linear/
302
+ // JUnit/webhook/marketplace packages). Declared in comber.config.json; each delivery is gated
303
+ // by severity + isolated so one connector failure never breaks the run.
304
+ const connectors = await ConnectorFanout.load(cfg.connectors);
305
+ if (connectors.active.length) {
306
+ await connectors.deliver(result, runMeta);
307
+ console.log(` connectors: ${connectors.active.join(", ")}`);
308
+ }
309
+ const fails = result.findings.filter((f) => f.severity === "fail").length;
310
+ const warns = result.findings.filter((f) => f.severity === "warn").length;
311
+ const infos = result.findings.filter((f) => f.severity === "info").length;
312
+ console.log(`\n${fails ? "❌" : "✅"} ${fails} fail / ${warns} warn / ${infos} info across ${result.visitedStates} states ` +
313
+ `(${result.actionsTaken} actions, ${(result.durationMs / 1000).toFixed(1)}s, ` +
314
+ `${result.tokenSpend.output} out-tokens${result.cappedBy ? `, capped by ${result.cappedBy}` : ""})`);
315
+ if (result.coverage) {
316
+ const c = result.coverage;
317
+ const x = (k) => c.extra[k] ?? 0;
318
+ const followTargets = x("followTargetsDiscovered") || x("linksDiscovered");
319
+ const denom = Math.max(c.elementsExercised, c.elementsDiscovered - c.elementsSkipped - followTargets);
320
+ const blockerBlindSpots = c.blindSpots.filter((blindSpot) => blindSpot.severity === "blocker-for-exhaustive").length;
321
+ const exhaustiveSuffix = c.exhaustiveClaimAllowed
322
+ ? ""
323
+ : c.cappedNotExhaustive
324
+ ? " - not exhaustive (capped)"
325
+ : " - not exhaustive";
326
+ console.log(` coverage: ${c.elementsExercised}/${denom} elements exercised (${c.elementCoveragePct}%) · ` +
327
+ `${x("controlsExercised")} controls · ${x("modalsOpened")} modals · ` +
328
+ `links ${x("linksChecked")} checked/${x("linksBroken")} broken/${x("linksDiscovered")} found · ` +
329
+ `${x("branchesExercised")} branches/${x("branchTargetsDiscovered")} child states, ` +
330
+ `${c.frameBlindSpots} blind-spots/${blockerBlindSpots} blocker gaps, ` +
331
+ `${redacted.exhaustivenessLedger.length} ledger rows` +
332
+ exhaustiveSuffix);
333
+ const skips = Object.entries(c.skipLedger);
334
+ if (skips.length)
335
+ console.log(` skipped: ${skips.map(([k, v]) => `${k}=${v}`).join(", ")}`);
336
+ }
337
+ if (result.baseline) {
338
+ const b = result.baseline.summary;
339
+ console.log(` baseline: ${b.newFail} new fail / ${b.newWarn} new warn / ${b.resolved} resolved / ${b.unchanged} unchanged` +
340
+ (result.baseline.baselineFound ? "" : " (no prior baseline found)"));
341
+ }
342
+ console.log(" covered:");
343
+ for (const u of redacted.visitedUrls)
344
+ console.log(` · ${u}`);
345
+ console.log(`📄 ${reportPath}`);
346
+ // Default soft (non-blocking, per the rollout plan); --strict gates CI on fails.
347
+ process.exit(strict && fails > 0 ? 1 : 0);
348
+ }
349
+ main().catch((err) => {
350
+ console.error("comber crashed:", err);
351
+ process.exit(2);
352
+ });