Documentation
¶
Overview ¶
Package localcmd runs one local operating-system command under hard limits.
It is a leaf package: it imports nothing from the rest of Joro, the same shape internal/jsruntime has and for the same reason. Everything a command may see arrives through a Spec and a Substitutions, so there is no field on any type here through which a command could reach a capture store, a token file, or an HTTP client.
Not internal/shell ¶
internal/shell also holds a command "executor". That one runs commands on a *remote* target through a web shell Joro deployed, over HTTP, with the scope guard applied to the target URL. This package runs a command on the operator's *own machine*. The two are unrelated and the names are close enough to confuse, which is why this one says "local" in it.
What this contains, and what it does not ¶
A command is not sandboxed. It is a process running as the operator with the operator's filesystem and network, and nothing here changes that — Joro's own threat model already says as much: any process running as the operator can drive the whole API. What this package contains is the realistic failure. A command cannot run forever, cannot fill memory, cannot return unbounded output, cannot write into the operator's working directory, and cannot be steered by the bytes it is fed.
That last one is the part that takes work, and it is what most of this file is.
The argv rule, precisely ¶
The property that matters is not "captured bytes never reach argv". It is this:
The number of argv elements, their boundaries, and the identity of the program are fixed before any wire-derived byte is known. At run time a value can only be interpolated *into* an element that already exists.
That is what the list form of Spec.Args buys, and it is what closes the injection class captured traffic makes live: substitute runs over an array that already exists, so a value holding a space, a quote, a semicolon or a newline still lands in exactly one element and cannot split, merge, create or reorder one, nor change which program runs.
Given that property, bytes in argv are a question of cost rather than of safety, and the costs are bounded rather than avoided. A small value derived from the wire — a host, a URL, a status — is held to a grammar for its name. The transaction's own bytes reach a command on stdin, in a file, or — when the operator writes {{INPUT}} into an argument — inline in argv, where three things are true and are bounded accordingly:
- Length. A platform refuses an over-long argument at exec time (Linux caps one argument at MAX_ARG_STRLEN, Windows the whole command line at 32767), so the inline budget sits below that and refuses first, with a reason naming the alternative.
- Representability. argv is NUL-terminated, and Windows re-encodes it to UTF-16, so a NUL is impossible and invalid UTF-8 corrupts. Both are refused.
- Confidentiality. argv is readable by other processes on this host, through /proc/<pid>/cmdline and ps. Stdin is not. This one cannot be engineered away, so it is stated where the operator chooses it rather than bounded here.
See Substitutions, substitute and checkBulk.
One thing argv validation does not cover on its own, because it is a property of the target rather than of the value: Windows runs a .bat or .cmd through cmd.exe, which parses the command line by different rules than the ones Go escapes for. cmdline.go closes that, and the two have to be read together — the guarantee that a captured URL is safe in argv rests on there being no shell in the path, on either platform.
The author-time tokenizer ¶
The editor offers a single command box rather than one input per argument, and web/src/lib/cmdline.ts is what splits that text into a program and an argument list. It runs in the browser, before storage, and its output is argv: the wire shape, the manifest on disk and every type here are unchanged, and no field anywhere takes a command line as one string. Tokenizing at author time is precisely what keeps the property above true — the split is applied to the template, never to a resolved value.
Permissions ¶
The scratch tree is created 0700 and its files 0600. Those are advisory on Windows, where Go maps a mode onto little more than the read-only bit; there the profile directory's own ACL is what keeps the tree private, as it already is for the rest of ~/.joro.
Index ¶
Constants ¶
const ( // DefaultTimeout is longer than a script's 25 seconds because the tools this // exists for are slower: a scanner or a fetch against a live target is measured in // tens of seconds even when nothing is wrong. DefaultTimeout = 60 * time.Second DefaultMaxStdoutBytes = 256 << 10 StockMaxStdoutBytes = 4 << 20 DefaultMaxStderrBytes = 64 << 10 StockMaxStderrBytes = 1 << 20 DefaultMaxArtifactBytes int64 = 16 << 20 StockMaxArtifactBytes int64 = 256 << 20 // DefaultMaxInlineInputBytes is what {{INPUT}} may put into argv when nobody said // otherwise. Generous enough for a request, which is the case inline input exists // for, and short of a response body, which is the case that should be piped. // // There is no "off" value, deliberately. Every other field here reads zero as // "unset, take the default", and one field where zero means something else would be // a trap; the coarse switch already exists and is --automation-commands. Lowering // this is how an operator narrows what can appear in ps. DefaultMaxInlineInputBytes = 64 << 10 )
The numbers Joro ships. Each field has two: the default a run gets when nobody said otherwise, and the maximum a run may ask for when the operator has named none.
Stock maxima are not a bound on the operator, they are what applies in their absence. The only real ceilings are the structural ones below, each tied to a number elsewhere that cannot move at runtime.
const ( // CapTimeout bounds a run because a run is the synchronous body of // POST /api/v1/automation/runs and the browser client gives up at 630 seconds. A // command permitted to outlive that would be killed by a client timeout instead of // reporting a termination reason, which is the one outcome worth preventing — // the operator would see a network error where a timeout belongs. CapTimeout = 10 * time.Minute // CapConcurrentRuns bounds overlapping command runs. Low, and lower than the // script ceiling, because the cost is different in kind: a script run is bounded by // a heap limit inside a process Joro controls, while each command run is a whole // process with its own memory ceiling and its own network activity. CapConcurrentRuns = 4 // CapScratchRuns is the run log's own size. Keeping a scratch directory for a run // that has already fallen off the log leaves files on disk that nothing in the UI // can reach, which is a leak rather than a retention policy. CapScratchRuns = 50 )
The structural ceilings: the figures an operator cannot raise, each because something outside this budget is fixed against it. Every one is reported to the UI with its reason, so a field is never presented as free when it is not.
const ( // DefaultConcurrentRuns is one. A command automation firing on captured traffic is // the case that matters, and serializing it is what keeps a burst of requests from // becoming a burst of processes. DefaultConcurrentRuns = 1 // DefaultScratchRuns is how many runs keep their scratch directory, newest first. DefaultScratchRuns = 10 )
Defaults for the host half of the budget: limits an operator sets once for this Joro rather than per run, and which nothing may ask to change.
const ( ReasonSuccess = "success" // ReasonExitStatus means the command ran and exited non-zero. Not an error in // Joro — a search program exits non-zero when it matches nothing — so the exit code // is reported and the caller decides what it meant. ReasonExitStatus = "exit status" ReasonTimeout = "timeout" ReasonCancelled = "cancelled" // ReasonOutputLimit means the command produced more than its output budget and // was stopped. Distinct from a truncated success: output that hit the ceiling // mid-stream is not a complete answer, and reporting it as one would be worse // than saying so. ReasonOutputLimit = "output limit" // ReasonSpawnFailed means the process never started — a missing or unexecutable // binary, or a scratch directory that could not be made. ReasonSpawnFailed = "spawn failed" // ReasonNotPermitted means Joro refused before spawning: a placeholder that did // not validate, a disabled feature, an unresolvable spec. ReasonNotPermitted = "not permitted" // ReasonInputTooLarge means {{INPUT}} would have put more bytes into argv than the // inline budget allows, so the run was refused before spawning. // // Its own reason rather than a shade of ReasonNotPermitted, because the two send an // operator to different places: "not permitted" reads as a missing grant or a // disabled feature, where this one has two real fixes — raise the budget, or pipe // the input on stdin instead, which has no size limit at all. ReasonInputTooLarge = "input too large" // ReasonRuntimeFailure is ours, not the command's. ReasonRuntimeFailure = "runtime failure" )
Termination reasons. Every run ends with exactly one, and the operator sees it verbatim, so they are phrased as outcomes rather than error classes. Same two-field pattern as jsruntime: prose here, a stable code below.
const ( OutcomeSuccess = "success" OutcomeExitStatus = "exit_status" OutcomeTimeout = "timeout" OutcomeCancelled = "cancelled" OutcomeOutputLimit = "output_limit" OutcomeSpawnFailed = "spawn_failed" OutcomeNotPermitted = "not_permitted" OutcomeInputTooLarge = "input_too_large" OutcomeRuntimeFailure = "runtime_failure" // OutcomeUnknown is what an unmapped reason resolves to, so the mapping fails // safe: a reason added without a code here reports a run whose fate is // unrecognized rather than a run that succeeded. OutcomeUnknown = "unknown" )
Outcome codes pair one-to-one with the reasons above. A reason is prose the operator reads and is free to be reworded; an outcome is an identifier a program branches on and is therefore not.
const ( StdinNone = "none" StdinRequest = "request" StdinResponse = "response" StdinBoth = "both" // StdinTrigger feeds the trigger payload as JSON. It is what a detect.finding or // fuzzer.complete trigger has to offer: those carry no single transaction, so // there are no request bytes to pipe. StdinTrigger = "trigger" )
Stdin modes: what the command reads on its standard input.
const ( OutputText = "text" OutputJSON = "json" )
Output modes: how a lens tab should render stdout.
const ( PartRequest = "request" PartResponse = "response" PartBoth = "both" PartTrigger = "trigger" )
Part names for Files, matching the Stdin vocabulary that overlaps with it.
const ( MaxPathLen = 512 MaxArgs = 64 MaxArgLen = 4096 MaxFiles = 8 MaxEnvVars = 32 MaxEnvName = 128 MaxEnvValue = 4096 )
Shape limits on a spec. These bound what an operator can declare, not what a command can do; they exist so a hand-edited or generated manifest cannot produce an argv nobody can read or an environment nobody reviewed.
const ( PlaceholderInput = "INPUT" PlaceholderScratch = "SCRATCH" PlaceholderHost = "HOST" PlaceholderURL = "URL" PlaceholderMethod = "METHOD" PlaceholderStatus = "STATUS" PlaceholderSeq = "SEQ" PlaceholderFindingID = "FINDING_ID" PlaceholderCampaignID = "CAMPAIGN_ID" )
The placeholders Joro supplies. Declared here so validateShape can accept an argument that names one and reservedPlaceholder can refuse a Files key that shadows one.
const CapInlineInputBytes = 96 << 10
CapInlineInputBytes is the most {{INPUT}} may put into argv on this platform.
Linux refuses a single argument over MAX_ARG_STRLEN, which is 32 pages — 128 KiB — and returns E2BIG from exec with nothing to say about which argument was at fault. macOS applies ARG_MAX to the whole block instead, at 1 MiB. This sits under the lower of the two with room for the rest of the command line, so the budget refuses first and names the fix rather than letting the kernel report a spawn failure.
Variables ¶
var ErrInlineTooLarge = errors.New("inline input over budget")
ErrInlineTooLarge is what substitute returns when {{INPUT}} would put more bytes into argv than the run allows. A sentinel because Run maps it to its own termination reason: "not permitted" would send an operator looking for a grant, where the two real fixes are a larger budget or stdin.
var FileParts = []string{PartRequest, PartResponse, PartBoth, PartTrigger}
FileParts lists what Files may materialize.
var OutputModes = []string{OutputText, OutputJSON}
OutputModes lists the valid Spec.Output values.
var StdinModes = []string{StdinNone, StdinRequest, StdinResponse, StdinBoth, StdinTrigger}
StdinModes lists the valid Spec.Stdin values, in the order the UI shows them.
Functions ¶
func OutcomeFor ¶
OutcomeFor returns the stable code for a termination reason.
func ReservedPlaceholders ¶
func ReservedPlaceholders() []string
ReservedPlaceholders lists what Joro supplies, for a validation message and for the editor's reference panel.
Types ¶
type Artifact ¶
type Artifact struct {
Name string `json:"name"`
Bytes int64 `json:"bytes"`
// Dropped means the file was past the run's artifact budget and has been deleted.
// Still reported, because a scanner whose report directory did not fit should say
// so rather than appear to have written nothing.
Dropped bool `json:"dropped,omitempty"`
}
Artifact is one file the command left in its scratch directory.
Name is relative to the scratch root and is what a download route takes, so it is held to a shape that cannot escape that root even before the route re-validates it.
type Budget ¶
type Budget struct {
TimeoutMs int `json:"timeoutMs,omitempty"`
MaxStdoutBytes int `json:"maxStdoutBytes,omitempty"`
MaxStderrBytes int `json:"maxStderrBytes,omitempty"`
MaxArtifactBytes int `json:"maxArtifactBytes,omitempty"`
MaxInlineInputBytes int `json:"maxInlineInputBytes,omitempty"`
}
Budget is the operator-facing and wire form of Limits: the same figures in the units a manifest declares and a form edits.
func DefaultBudget ¶
func DefaultBudget() Budget
DefaultBudget and StockMaxima report this package's own numbers in operator units, so the UI can state them without a second copy of the table.
func StockMaxima ¶
func StockMaxima() Budget
StockMaxima carries no TimeoutMs: the wall-clock maximum is CapTimeout rather than a stock figure below it, and the spec reports that as a Cap instead.
func (Budget) Limits ¶
Limits converts to the runtime's own units. Not normalized: a zero stays a zero, so a caller can still tell "unspecified" from a real value.
func (Budget) Value ¶
Value reports one field by its BudgetSpec key, and whether the key is known.
Paired with BudgetSpecs so a caller can validate the whole budget without keeping a second list of fields: a sixth field added above without a case here fails loudly at its validator rather than reading as zero and passing unchecked.
type BudgetSpec ¶
type BudgetSpec struct {
Key string `json:"key"`
Label string `json:"label"`
Unit string `json:"unit"`
Factor int `json:"factor"`
Default int `json:"default"`
DefaultMax int `json:"defaultMax,omitempty"`
Cap int `json:"cap,omitempty"`
CapReason string `json:"capReason,omitempty"`
Description string `json:"description"`
}
BudgetSpec documents one configurable field for the operator.
Field-for-field identical to jsruntime.BudgetSpec, deliberately: the frontend renders both budgets with one component, and a second shape would mean a second component.
func BudgetSpecs ¶
func BudgetSpecs() []BudgetSpec
BudgetSpecs describes the per-run fields, in the order they should be read: how long, what Joro keeps of the result, then what it will put in the command line.
func HostSpecs ¶
func HostSpecs() []BudgetSpec
HostSpecs describes the two fields that belong to this Joro rather than to one run. They have no DefaultMax, because nothing can ask for another value: the operator's number is the limit.
type HostBudget ¶
type HostBudget struct {
ConcurrentRuns int `json:"concurrentRuns,omitempty"`
ScratchRuns int `json:"scratchRuns,omitempty"`
}
HostBudget is the half of the policy that is a property of this Joro rather than of one run.
func (HostBudget) Resolved ¶
func (h HostBudget) Resolved() HostBudget
Resolved fills each unset field with its shipped default and holds every field to its ceiling, so a caller can use the result without checking anything.
type Limits ¶
type Limits struct {
Timeout time.Duration
MaxStdoutBytes int
MaxStderrBytes int
// MaxArtifactBytes bounds the total the scratch directory may hold when artifacts
// are collected. A command that fills the disk is the operator's problem either
// way; this bounds what Joro then reads and retains.
MaxArtifactBytes int64
// MaxInlineInputBytes bounds what {{INPUT}} may put into argv, across the whole
// argument list. Zero switches inline input off rather than taking a default, which
// is the one place a zero here does not mean "unset" — see Fill.
MaxInlineInputBytes int
}
Limits bound one run, in the runtime's own units. A zero field takes the default.
There is no memory field; see the header of budget.go for why the process boundary already provides what one would buy.
func (Limits) Fill ¶
Fill supplies a default for any field left at zero and enforces the absolute caps.
Deliberately does not apply the stock maxima — the same split jsruntime.Limits.Fill documents. Those maxima bound what a caller may *ask for*, and that question is settled once against the operator's policy before Limits reaches this package. By the time it does, the numbers are a resolved budget rather than a request, so re-applying a stock maximum here would silently lower a limit the operator raised.
func (Limits) Normalize ¶
Normalize resolves a request against no policy at all: Joro's own defaults and stock maxima. It is what a caller with no operator policy in hand gets.
func (Limits) NormalizeWith ¶
NormalizeWith resolves a request against the operator's policy, clamping each field between what they said a run gets and the most one may ask for.
type PlaceholderDoc ¶
type PlaceholderDoc struct {
Name string `json:"name"`
Token string `json:"token"`
Description string `json:"description"`
Grammar string `json:"grammar"`
// Source is the trust class as a word the UI can group on: "joro" for what Joro
// computed, "captured" for a validated value off the wire, "input" for the
// transaction's own bytes.
Source string `json:"source"`
}
PlaceholderDoc is one placeholder as the editor's reference table shows it.
func PlaceholderDocs ¶
func PlaceholderDocs() []PlaceholderDoc
PlaceholderDocs describes every placeholder Joro supplies, in the order the editor lists them.
It says what each one is and what a valid value looks like. It does not say which triggers supply which — that is not this package's knowledge, and jsautomation.CommandPlaceholderAvailability holds it beside the function that decides it.
type Policy ¶
type Policy struct {
Defaults Budget `json:"defaults,omitzero"`
Maxima Budget `json:"maxima,omitzero"`
Host HostBudget `json:"host,omitzero"`
}
Policy is everything the operator sets about command runs: what a run gets by default, the most it may ask for, and the host limits that are neither requestable nor declarable.
type Result ¶
type Result struct {
Reason string `json:"reason"`
Outcome string `json:"outcome"`
// Err carries Joro's own explanation when the run did not happen or could not be
// completed. Never a Go error string with a wrapped chain — the audience is a
// person reading the run log.
Err string `json:"err,omitempty"`
// ExitCode is the process's exit status, or -1 when it never reported one
// (killed, or never started).
ExitCode int `json:"exitCode"`
Stdout []byte `json:"-"`
Stderr []byte `json:"-"`
StdoutTruncated bool `json:"stdoutTruncated,omitempty"`
StderrTruncated bool `json:"stderrTruncated,omitempty"`
Artifacts []Artifact `json:"artifacts,omitempty"`
// Limits is what this run was actually held to, after the operator's policy was
// applied. Reported rather than left implicit, for the same reason jsruntime
// reports it: a truncation flag with nothing to read it against explains nothing.
Limits Budget `json:"limits"`
DurationMs int64 `json:"durationMs"`
}
Result is the outcome of a run.
Always returned, including on failure: a command that timed out still has whatever it wrote before it was stopped, and that is usually what explains it.
func Run ¶
Run executes one command and returns what happened.
The returned error means the run could not be attempted at all — no scratch directory, a spec this package cannot make sense of. Everything else, including a command that never started, timed out, was killed or exited non-zero, comes back as a Result carrying its reason. A caller therefore has one thing to report and one place to look, which is the contract jsautomation.Manager.Run already relies on.
type RunOpts ¶
type RunOpts struct {
// Limits is the resolved budget. Fill is applied here, not NormalizeWith: by the
// time a caller reaches this package the operator's policy has already been
// applied, and re-normalizing would silently lower a limit they raised.
Limits Limits
// Scratch is the run's working directory. The caller creates it 0700 and owns its
// lifetime; this package sets it as the process's Dir, writes materialized inputs
// into it, and collects what the command left behind.
//
// Required. A command with no scratch directory would inherit Joro's own working
// directory, which is wherever the operator started the binary — so a scanner
// writing a report directory would write it into their source tree.
Scratch string
// Inputs names the files this package wrote into Scratch before the command
// started, so they are not reported back as things the command produced.
Inputs []string
// Subst resolves the {{PLACEHOLDER}} references in Spec.Args.
Subst Substitutions
// ProxyURL and CAFile are used only when Spec.UseProxy is set. ProxyURL is a full
// URL ("http://127.0.0.1:8080"); CAFile is a path to Joro's CA certificate in PEM
// form, so a tool that honours the conventional variables can verify Joro's
// interception instead of failing the handshake.
ProxyURL string
CAFile string
}
RunOpts is everything about one run that is not the spec itself.
type Spec ¶
type Spec struct {
// Path is the executable. Validate resolves it through exec.LookPath and rewrites
// this field to the absolute result, so the binary the operator reviewed is the
// binary that runs. A bare name looked up again at run time would be shadowable
// from whatever directory Joro happened to be started in.
Path string `json:"path"`
// Args are the arguments, after {{PLACEHOLDER}} substitution. Placeholders resolve
// only to values Joro computed or validated; see substitute.
Args []string `json:"args,omitempty"`
// Stdin selects what the command reads on its standard input. Unbounded and
// binary-safe, and invisible to other processes on the host — the delivery to reach
// for when either of those matters.
Stdin string `json:"stdin,omitempty"`
// Inline names the source for {{INPUT}} when it appears in an argument, using the
// same vocabulary as Stdin. Empty means no argument may name {{INPUT}}.
//
// A field of its own rather than an overload of Stdin, so Stdin's meaning — what the
// program reads — stays true, and so a spec can legitimately do both. It is absent
// on every package installed before inline input existed, which is exactly the right
// default: {{INPUT}} in an argument is refused unless something says where it comes
// from, and validateShape says so by name.
//
// What this costs relative to Stdin, and what bounds it, is in the package header
// under "The argv rule, precisely". The short version: bounded by the run's inline
// budget, text only, and visible in ps.
Inline string `json:"inline,omitempty"`
// Files materializes parts of the transaction into the run's scratch directory
// before the command starts. The key is the placeholder that resolves to the
// written file's absolute path; the value is which part to write.
//
// This is what makes a program that wants a file rather than a pipe work — one whose
// interface is `-r <file>` rather than stdin — without the bytes ever touching argv.
Files map[string]string `json:"files,omitempty"`
// Env is added to a minimal base environment. EnvPass names variables inherited
// from Joro's own environment.
//
// A whitelist rather than an inheritance, which is the opposite of what
// jsruntime's worker does. That worker inherits Joro's environment and its doc
// comment says this changes nothing about the sandbox — true there, because the
// JavaScript global object holds no accessor for an environment. A command has
// one, so inheriting would put every variable in Joro's environment, including
// whatever cloud or API credentials the operator's shell exports, into every run.
Env map[string]string `json:"env,omitempty"`
EnvPass []string `json:"envPass,omitempty"`
// UseProxy routes the command's HTTP traffic through Joro's own proxy, by setting
// the conventional proxy and CA-bundle variables in its environment.
//
// It is a default, not a control. A command can unset them, and a tool that does
// its own dialing without consulting them was never bound in the first place —
// the capability guard's scope rule cannot reach a subprocess. What this buys,
// when a command does honour them, is that its traffic lands in History under
// the same scope, noise and Match & Replace the operator's browser gets, which is
// the same argument the managed testing browser is built on.
UseProxy bool `json:"useProxy,omitempty"`
// Redact masks credential header values in whatever reaches the command, by any of
// the three deliveries: stdin, a file, or an inline {{INPUT}}.
//
// Off by default because the usual reason to pipe a request somewhere is to replay
// it, and replaying an authenticated request with a masked Cookie tests an anonymous
// endpoint and reports a false negative. On for a command that sends the bytes
// somewhere Joro does not control.
Redact bool `json:"redact,omitempty"`
// Output tells a lens tab how to render stdout. Ignored for a run that is not a
// lens, which has the run log rather than a tab.
Output string `json:"output,omitempty"`
}
Spec is what to run.
Args is a list and there is no field that takes a command line as one string. That is the single most important property in this package, and the package header states it exactly: the shape of argv is fixed before any wire byte is known, so a value holding a semicolon, a backtick or a newline is one argument rather than the start of another. Adding a `shell` or `command` string field for convenience would reintroduce the whole injection class, and would look like a small ergonomic improvement while doing so — the editor's single command box is not that field, because it splits in the browser at author time and stores the result here as a list.
func (*Spec) Normalize ¶
func (s *Spec) Normalize()
Normalize fills defaults and trims. Called before Validate so a spec that omits optional fields is accepted rather than corrected by the operator.
func (Spec) Render ¶
Render returns the canonical text of a spec: what will run, one fact per line.
This is a command package's "source", and it carries the same weight a script's does. The run log retains it verbatim so an operator reviewing what happened reads the actual argv rather than a description of it, and its hash is what the revision history tracks — so changing an argument cuts a revision while changing a description does not, because a description is not here.
Deterministic: maps are emitted in sorted key order, so an unchanged spec always hashes the same regardless of Go's map iteration.
One fact per line, and it stays that way. Rendering the joined command line the editor shows would be more readable and would destroy what this is for: a joined line is ambiguous about where one argument ends, which is exactly the question a reviewer is reading the run log to answer. Spec.Summary is the lossy display form; this is not.
A field added here changes every installed package's hash and cuts a revision on each, so a new one is emitted only when it is set — the reason the inline line below is conditional rather than always present like stdin.
func (Spec) Summary ¶
Summary is the one-line form, for a list view.
Lossy and display-only: it joins with spaces, so an argument containing a space reads as two and an empty one disappears. Fine for a list row, wrong for anything that has to be read back — Render is the canonical form, and the editor's own joiner (web/src/lib/cmdline.ts) is the one that round-trips.
func (*Spec) Validate ¶
Validate reports why a spec cannot be installed, and resolves Path to an absolute executable on success.
It runs at install time rather than at first run, which is the same choice jsautomation makes by compiling a script on install: a missing binary or a typo in a placeholder is reported to whoever submitted the package while they are still looking at it, instead of surfacing hours later as a trigger that quietly fails.
Messages name the field and the rule, because the audience is as often a person reading a validation error in a form as it is a reviewer.
type Substitutions ¶
type Substitutions struct {
// Trusted holds values Joro produced: the scratch directory, and the absolute
// path of each file it wrote there. Nothing from the wire belongs here.
Trusted map[string]string
// Captured holds values derived from a captured transaction or an event: HOST,
// URL, METHOD, STATUS, SEQ, FINDING_ID, CAMPAIGN_ID. Each is validated by
// checkCaptured against the grammar for its name.
Captured map[string]string
// Bulk holds the transaction's own bytes, for {{INPUT}}.
//
// A third class because it is neither of the others: far too large for the value
// cap a Captured entry lives under, and with no grammar available — an HTTP message
// is whatever the target chose to send, so there is nothing to hold it to. What
// stands in is a budget and a representability check; see checkBulk.
Bulk map[string]string
}
Substitutions are the placeholder values for one run, split by where they came from.
Three maps rather than one, so the trust boundary is a property of the type instead of a rule someone has to remember. Trusted values go into argv as they are; Captured values are held to a grammar first, because a response body an operator is looking at came from the target and a URL path is exactly the kind of thing a stored payload controls; Bulk holds the transaction's own bytes, which have no grammar to be held to.