/** * Shared rendering machinery for the offline export-plan emitters. * * The emitters render a shell script the USER runs, outside the fence, with * the user's own credentials. Four properties are load-bearing, and every * helper here exists to hold one of them: * * 1. **The script draws the same sample the live connector draws.** Bucket * count, per-bucket cap and page size come from the connector modules * (`CLOUDWATCH_BUCKET_COUNT` and friends), not from constants retyped * here. A fenced POC and a credentialed POC over the same window should * differ because the logs differ, never because two samplers disagreed. * * 2. **A reviewer can read it once and be done.** The calls are * stereotyped: a fixed preflight, a fixed loop, one read-only API per * script. Bucket ranges are rendered as literal timestamps in a comment * table so the reviewer can see the whole time footprint without running * anything, and every value that came from tool arguments is * single-quoted through `shQuote`. * * 3. **Nothing in it points at us.** No log10x hostname appears in any * emitted script; `assertNoVendorHost` enforces that at render time and * a regression test enforces it per SIEM. * * 4. **Credentials never reach argv.** `renderCurlHeaders` writes them to a * 0700 scratch file that the EXIT trap removes, because `ps` shows every * user on the machine every argument of every process. The live * connectors send these headers inside an HTTP client, and the script * should not leak what the credentialed path does not. * * Output shape: plain text, one log message per line, one file per source the * vendor API already enumerates. Plain text rather than JSONL because * `log10x_poc_from_local`'s multi-file lane feeds lines to the templater * verbatim — a JSON wrapper there gets tokenized as if the wrapper were the * log (the measured requirement in `lib/local-file-source.ts`), so the * unwrapping belongs in this script, in `jq`, where the user can see it. */ import { type SamplingBucket } from '../_sampling.js'; /** SIEMs with an export-plan emitter. The rest are listed as follow-ups. */ export declare const EXPORT_PLAN_SIEMS: readonly ["cloudwatch", "splunk", "elasticsearch", "opensearch", "datadog"]; export type ExportPlanSiemId = (typeof EXPORT_PLAN_SIEMS)[number]; /** * SIEMs a fenced POC cannot export from yet. Named rather than silently * absent: a prospect on ClickHouse should be told "not this one, here is * what exists" instead of watching their SIEM fail an enum check. */ export declare const EXPORT_PLAN_FOLLOW_UPS: ReadonlyArray<{ id: string; displayName: string; }>; /** Default target, deliberately identical to `log10x_poc_from_siem`'s. */ export declare const DEFAULT_TARGET_EVENT_COUNT = 1000000; /** Where the emitted scripts write, relative to the user's working directory. */ export declare const DEFAULT_OUTPUT_DIR = "./poc/logs"; export interface SamplePlanOptions { siem: ExportPlanSiemId; /** `1h`, `24h`, `7d`, `14d`, `30d` — same grammar as the connectors. */ window: string; targetEventCount: number; /** SIEM-native resource scope: log-group prefix, index, index pattern. */ scope?: string; /** SIEM-native filter layered on top of `scope`. */ query?: string; outputDir?: string; /** Injected by tests so golden files are stable. Defaults to now. */ nowMs?: number; /** Injected by tests so bucket offsets are stable. Defaults to Math.random. */ rng?: () => number; } export interface SamplePlan { siem: ExportPlanSiemId; displayName: string; /** Suggested filename for the script. */ filename: string; /** The script itself. */ script: string; bucketCount: number; perBucketCap: number; targetEventCount: number; window: string; outputDir: string; /** Command-line tools the script needs on the user's machine. */ requires: string[]; /** Credential environment variables the script reads. */ credentials: string[]; /** Read-only API operations the script calls, for the review pass. */ apiCalls: string[]; /** * Arguments to hand `log10x_poc_from_local` afterwards, sized so the * whole exported sample is read rather than a fraction of it. */ pocFromLocalArgs: Record; notes: string[]; } /** * Quote a value for POSIX shell single-quoting. * * Every tool argument that reaches a rendered script goes through here. A * scope of `'; curl evil.example` is then a log-group name that matches * nothing, which is the correct outcome — the emitted script is text the user * reads before running, and a value that could rewrite the script's structure * would defeat the reading. */ export declare function shQuote(value: string): string; /** Strip a value down to something safe to use as a filename stem. */ export declare function fileStem(value: string): string; /** * Refuse to return a script that would contact us. * * The guarantee the fenced profile sells is that the export step reaches the * user's analyzer and no one else. A log10x address in the emitted text would * break that guarantee where it is least likely to be noticed and most * damaging to find later, so it is a render-time failure, not a lint warning. */ export declare function assertNoVendorHost(script: string, siem: string): void; export interface BucketPlan { buckets: SamplingBucket[]; perBucketCap: number; fromMs: number; toMs: number; } /** * Build the bucket plan for a script, using the same sampler the connectors * use. Draws once, at emit time, so the rendered script carries literal * timestamps a reviewer can read instead of a randomizer they would have to * trust. */ export declare function planBuckets(opts: SamplePlanOptions, bucketCount: number): BucketPlan; /** `2026-08-27 13:04:11Z` — readable in a comment, unambiguous in a log. */ export declare function humanTime(ms: number): string; export interface HeaderInput { displayName: string; apiSummary: string; credentialSummary: string; writes: string; window: string; bucketCount: number; perBucketCap: number; targetEventCount: number; fromMs: number; toMs: number; buckets: SamplingBucket[]; /** Extra lines appended to the header, e.g. per-SIEM caveats. */ extra?: string[]; } export declare function renderHeader(h: HeaderInput): string; /** * Preflight block: refuse early and by name when a tool or credential is * missing, rather than half-writing an output directory and failing on the * first API call. */ export declare function renderPreflight(bins: string[], requiredEnv: Array<{ name: string; hint: string; }>): string; /** * Render the bucket ranges as two shell arrays. * * `unit` picks the literal form the vendor API wants: CloudWatch takes epoch * milliseconds, everything else takes ISO-8601. Both forms are followed by a * comment carrying the human-readable range, so the arrays stay reviewable * even in the epoch-millisecond case. */ export declare function renderBucketArrays(buckets: SamplingBucket[], unit: 'epoch_ms' | 'iso'): string; /** * Per-file ceilings the emitted scripts roll at. * * `log10x_poc_from_local`'s multi-file lane reads at most `per_pod_limit` * lines and 16 MB from each file it is given (`sampleFromFiles` → * `readStridedFileLines`); above either ceiling it strides, and a strided read * of one enormous file covers less of the pattern space than whole reads of * several. So the export script rolls to a new part file before it reaches * the ceiling, and the whole sample is read rather than sampled twice. * * 45,000 lines sits under the tool's 50,000-line maximum; 15 MB sits under * its 16 MB byte cap. At the default one-million-event target that is roughly * 23 part files, comfortably inside the 200-file maximum. */ export declare const MAX_LINES_PER_FILE = 45000; export declare const MAX_BYTES_PER_FILE = 15000000; /** `per_pod_limit` to pass to `log10x_poc_from_local` — its schema maximum. */ export declare const POC_PER_FILE_LIMIT = 50000; /** `max_pods` to pass to `log10x_poc_from_local` — its schema maximum. */ export declare const POC_MAX_FILES = 200; /** * Shell helper that names the file the next batch appends to, rolling to a * new part when the current one reaches either ceiling above. * * Stateless on purpose: it derives the part number from what is already on * disk rather than keeping a per-source counter. bash 3.2 (still the system * bash on macOS) has no associative arrays, and a helper a reviewer can read * top to bottom beats one that needs a data structure explained. */ export declare function renderOutfileHelper(): string; /** * Scratch directory + cleanup trap. Every emitter buffers one API response at * a time here rather than in a shell variable, so a 50,000-event page does * not become a 25 MB string in the shell's memory. * * `extra` names the additional scratch paths a given script uses. Declared * per script rather than all at once: an unused variable in a file whose * whole value is being readable in one pass is a question the reader has to * answer for nothing. */ export declare function renderScratch(extra?: Array<'curl_headers' | 'batch'>): string; /** * Write the credential-bearing headers to a file and hand curl `-H @file` * instead of `-H "Authorization: ..."`. * * Two reasons, and both are about the same thing — a script whose claim is * that it handles credentials carefully should not handle them carelessly. * * Arguments are visible in `ps` to every user on the machine. The live * connectors send these headers inside an HTTP client, where they never touch * a command line; a script that put a Splunk bearer token or a Datadog * application key in argv would leak a credential the SIEM path does not. * * And the file holds RAW header lines, not curl config directives. `--config` * was the first shape this took and it is a trap: curl's config parser accepts * `option: value` as well as `option = value`, so an unquoted * `header = DD-API-KEY: abc` is read as the option `header` with an empty * value and the header silently never goes out — measured against curl 8.7.1. * The quoted form works but then processes backslash escapes inside the value, * so a token containing one would arrive mangled. `-H @file` has no quoting * rules at all: one header per line, verbatim. * * Values are substituted by `printf`, not by a shell heredoc, so nothing in a * token is expanded on the way in. The file lives in the `mktemp -d` scratch * directory, which is 0700 and removed by the EXIT trap. * * Each entry is a header line with one `%s`, and the shell expression that * fills it: `{ header: 'Authorization: Bearer %s', value: '"$SPLUNK_TOKEN"' }`. */ export interface CurlHeaderEntry { header: string; value: string; } export declare const CURL_HEADERS_RATIONALE: string; export declare function renderCurlHeaders(entries: CurlHeaderEntry[], opts?: { indent?: string; rationale?: boolean; }): string; /** * The closing block every script shares: report what landed on disk, and name * the next step without naming a host. */ export declare function renderFooter(outputDir: string): string;