xila/v1 — data model and module contracts
Status: implemented in core/. This document is the contract every module,
rule pack and product codes against. Breaking it requires a schema bump
(xila/v2), not a patch.
1. Objects
All objects live in core/xila (package xila) and carry
Schema: "xila/v1".
| Object | Go type | Purpose |
|---|---|---|
| Artifact | xila.Artifact |
something submitted for analysis |
| Component | xila.Component |
a typed piece extracted from an artifact |
| Finding | xila.Finding |
a security issue with evidence |
| Evidence | xila.Evidence |
proof attached to a finding, already redacted |
| Signal | xila.Signal |
a runtime event for Guard |
| Assessment | xila.Assessment |
a decision about an artifact or signal |
| Run | xila.Run |
one execution of a pipeline, for reports |
Identifiers are deterministic (art_, cmp_, fnd_, sig_, asm_ +
16 base32 chars derived from content). The same artifact scanned twice in CI
produces the same finding IDs, so results diff, deduplicate and suppress
cleanly. Finding.Fingerprint is the stable cross-run key used for SARIF
partialFingerprints.
Component content
Component is the serialisable record; bytes come from Component.Content(),
which is not part of the JSON form.
type Content interface {
Open() (io.ReadCloser, error) // fresh reader
Bytes() ([]byte, error) // capped at xila.MaxComponentBytes (64 MiB)
Size() int64 // -1 if unknown
}
xila.FileContent(path) // lazy file-backed content
xila.BytesContent(b) // in-memory content
xila.Memoize(c) // read-once wrapper; applied by the host
Constructors:
c := xila.NewComponent(kind, logicalPath, extractorRef, content)
child := c.Child(kind, "scripts/login.gdc", extractorRef, content) // nested, sets Parent
b, err := c.Bytes() // nil, nil when the component has no bytes
Paths are relative to the artifact root, /-separated, and use ! between a
container and a member: game.pck!scripts/login.gdc.
Component kinds
The vocabulary shared by extractors (producers), analyzers (consumers) and
rules (applies_to):
managed-assembly, native-binary, config-file, script, asset-bundle,
string-table, il2cpp-metadata, project-settings, js-bundle,
source-map, wasm-module, web-asset, text, data-file, archive,
endpoint.
Artifact kinds: build.windows, build.linux, build.macos,
build.android, build.web, directory, file, archive, url.
Evidence and redaction
Redaction is centralised so no module can leak a secret into a report:
ev := xila.MatchEvidence(content, start, end, xila.RedactMatch)
RedactMatch(default): keeps the line, replaces the value withAIza…[redacted:39 chars]. Values ≤ 8 bytes are hidden entirely.RedactNone: quote as-is. Only for non-secrets (URLs, flags, versions).RedactLine: report the location only.
Excerpts are capped at 200 bytes and control bytes are replaced, so excerpts taken from binaries are safe to paste into a ticket.
2. Module contracts
core/module (package module). Built-in modules implement these interfaces
directly; external modules implement the same contract over JSON-RPC on
stdin/stdout and declare the same manifest in YAML.
type Manifest struct {
ID, Version, Schema string
Kind Kind // extractor | analyzer | prober | detector | reporter
Title, Description string
Engines []xila.Engine
Consumes, Produces []string
Permissions Permissions
Runtime *Runtime // external modules only
Community bool
}
type Extractor interface {
Manifest() Manifest
Supports(a xila.Artifact) bool
Extract(ctx context.Context, a xila.Artifact, out chan<- xila.Component) error
}
type Analyzer interface {
Manifest() Manifest
Accepts(kind xila.ComponentKind) bool
Analyze(ctx context.Context, c xila.Component) ([]xila.Finding, error)
}
type Prober interface {
Manifest() Manifest
Probe(ctx context.Context, t Target, s Scope) ([]xila.Finding, error)
}
type Detector interface {
Manifest() Manifest
Evaluate(ctx context.Context, signals []xila.Signal) (xila.Assessment, error)
}
type Reporter interface {
Manifest() Manifest
Format() string
Report(ctx context.Context, run xila.Run, findings []xila.Finding) error
}
Rules, not exceptions:
- Only extractors branch on engine. An analyzer that checks whether a file came from Unity is a bug; add a component kind or metadata key instead.
SupportsandAcceptsmust be cheap: the host calls them for every module.Extractmust not retainout.- Extractors send components as they find them; the host analyses them concurrently.
Permissions
type Permissions struct {
Network bool // Xila refuses these unless --allow-network
Filesystem FilesystemAccess // none | read-only-input | read-write-temp
Secrets []string // named host-held credentials
Exec bool // child processes; sandbox workers only
}
The host applies policy to declarations: it skips modules whose declared needs
are not allowed. Local external executables are not OS-sandboxed, so install
only trusted modules; cloud workers must enforce permissions with isolation.
Probers additionally require Scope.Authorization; the host refuses to run
active tests without it.
3. Host pipeline
core/plugin (package plugin).
h := plugin.New(plugin.Policy{Workers: 8}, plugin.Hooks{...})
h.MustRegister(myExtractor)
res, err := h.Scan(ctx, xila.NewArtifact(kind, root, label))
err = h.Report(ctx, "sarif", res.Run, res.Findings)
Pipeline: supporting extractors run concurrently into a bounded queue; a worker
pool runs every accepting analyzer over each component; findings are
deduplicated by fingerprint and sorted (severity, confidence, path, rule).
Module failures land in Run.Errors, policy skips in Run.Skipped; neither
aborts the scan. Only a cancelled context aborts.
Content is memoised per component, so N analyzers cause one read.
4. Rules
core/rules (package rules). Rules are YAML data; anything a rule cannot
express belongs in an analyzer.
rs, err := rules.LoadFS(rulepacks.FS(), ".") // embedded packs
rs, err := rules.LoadDir("./rules") // local packs
eng, err := rules.New(rs, rules.WithModule("secrets-analyzer@0.1.0"))
findings := eng.Match(ctx, component, content) // never errors
Layout: rules/<pack>/pack.yaml plus one rule per rules/<pack>/<name>.yaml,
fixtures in rules/<pack>/testdata/. Rule IDs are <pack>.<name>. testdata
is never loaded as rules and never embedded in the binary.
Rule shape:
id: secrets.gcp-service-account
title: Google Cloud service account key shipped in build
severity: critical # info | low | medium | high | critical
confidence: 0.97 # default 0.9
applies_to: [config-file, managed-assembly, script, text, string-table]
paths: ["**/*.json"] # optional glob filter, ** crosses / and !
exclude_paths: []
match:
all:
- contains: '"type": "service_account"'
- regex: '-----BEGIN (?:RSA )?PRIVATE KEY-----'
report: true # this leaf's matches become evidence
entropy: {min: 4.0, group: key, min_length: 20} # optional
redact: match # match (default) | none | line | <capture group>
remediation: Remove the key from the client, rotate it, move calls server-side.
references: ["https://xila.us/docs/rules/secrets.gcp-service-account"]
tags: [firebase, gcp, credentials]
allowlist:
regexes: [] # match value
line_regexes: [] # whole line
paths: [] # component path globs
stopwords: [] # case-insensitive substrings of the value
max_matches: 20
fixture: testdata/gcp-service-account.txt # default; must exist
negatives: []
Conditions: all, any, not, regex, contains, icontains,
path_glob, kind, min_size/max_size. Exactly one per node.
Regexes are RE2: no backreferences, no lookaround. This is deliberate —
RE2 is linear time, so a community rule cannot hang the scanner on a hostile
file. redact: <group> both hides and narrows the reported location to that
capture group.
Every rule ships a fixture it must match, and must not match the shared clean
corpus (rules/testdata/clean/).
5. Output
SARIF 2.1.0 is the native CI format (GitHub code scanning). JSON is the run record above. Markdown and text are for humans and start with a plain "what an attacker could extract from your game" summary. Reporters never re-redact: evidence arrives already safe.