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 with AIza…[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.
  • Supports and Accepts must be cheap: the host calls them for every module.
  • Extract must not retain out.
  • 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.