Operate

Running it as a going concern: validating evidence, scoring, reporting, and putting the whole thing on a schedule.

  1. Obligations, checks and lenses
  2. The validator
  3. Evidence registered at a URL
  4. Queue, then execute
  5. Scoring
  6. Reports
    1. Your own reports
  7. Changing data
  8. Jobs and run directories
    1. Where an artifact actually lives
    2. Pruning
  9. On a schedule
  10. Backups
  11. What is not covered yet

Obligations, checks and lenses

crane policy list --json     # the obligations, their checks and their weights
crane lens list              # the definitions of "product" in play

CRA ships as the first policy, alongside FedRAMP and the NTIA minimum elements. Each names its own checks, weights, reporting lens and the validator profile it depends on. Adding another obligation is a policy row and a profile file, not a code change.

A lens is a definition of what counts as a product. Four ship by default:

Lens Whose definition
cra.regulatory The regulatory denominator, and the default for leadership reporting
commercial What the business sells as a distinct purchasable unit
legal.oss What legal treats as a distributable unit for open source obligations
shiproom What the ship room reviews and approves as a release

A product can be in scope under one and out of scope under another, and both statements are kept. Every reported number names the lens it used. Do not reconcile them.

The validator

SBOM conformance is checked by ossbomer, a separate tool. Use the -ossbomer image and it is already there. Otherwise install it wherever the worker runs and point CRAne at it:

pipx install "ossbomer==2.2.0"
export CRANE_OSSBOMER_BIN=/path/to/ossbomer            # only if not on PATH
export CRANE_OSSBOMER_PROFILE_PATH=/opt/our-profiles   # your own regulation profiles
crane validate profiles                                # confirms the wiring

crane validate profiles exits 1 when a policy needs a profile this installation does not have, or when a profile it names has been withdrawn upstream. Run it after changing policies or the profile directory, and after upgrading the validator.

A withdrawn profile is worse than a missing one. ossbomer keeps withdrawn profiles in the catalog so old results stay explainable, and refuses to run them, so every validation fails as a tool error rather than a verdict and the check sits unmeasured while looking transient. This has happened once already: eu-cra-annex-vii was withdrawn for citing CRA Annex VII 8(a)-(c), which do not exist, and the CRA policy moved to eu-cra-annex-i.

Custom profiles are ossbomer’s own mechanism. A profile you write for another regulation is a YAML file in that directory, and an obligation policy referring to it by id is all CRAne needs to know about it.

If ossbomer is absent, validations fail saying the executable was not found and the job is not retried. Nothing is scored as failing on account of a tool that was never installed.

Evidence registered at a URL

A validation job reads local files and refuses a remote URI. That is deliberate: a job reaching the network would be a surprise on the machine it runs on. The refusal says to fetch the document first and register where it landed, and there is a command for that:

crane manage evidence-fetch --dry-run    # what would be fetched
crane manage evidence-fetch --by "Priya" # fetch it, under your network policy
crane validate sbom                      # now they can be read

Only SBOMs, only http and https, only documents reachable without credentials, and from the network of the host the command runs on. Each document is identified by content and stored under its hash in the uploads directory, exactly as an upload is, so the same document arriving both ways is one file. The artifact keeps its URI, which is where the document lives and came from, and gains the content hash, which is how the validator finds the local copy; the fetch is on the change record. A document that turns out to be a different format from the one it was registered as is refused and left alone. A fetch that fails marks the document inaccessible with the reason, which is the one place that state is literally true: somebody tried and could not see, and it is tried again next time. The job itself still never fetches, and neither does an import or a collection.

Queue, then execute

crane validate sbom --requested-by nightly --json   # queues, returns immediately
crane worker run --once --json                      # claims and runs, then exits

validate never runs a tool. It registers work for evidence that has not been validated at its current content hash, and returns. Re-queueing unchanged evidence queues nothing.

One exception. A run that timed out is a verdict on the budget, not on the document, so validate queues that document again. The budget is the job definition’s limit plus CRANE_OSSBOMER_SECONDS_PER_MB (default 60) for every megabyte of the file, so a large document gets more time than a small one rather than the same time and the same timeout. When one still runs out, the job’s error says how large the file was and what it was given; raise the rate on the worker and run validate again. The SBOM page counts these separately from documents nobody has validated yet, because waiting is a strategy for the second and not the first.

Prefer worker run --once over a long-lived process. There is nothing to keep alive and nothing to restart, and if one tick runs long the next cannot pick up the same job because the first still holds the lease. --follow exists for a container that must stay up.

Exit 5 from the worker means at least one job failed. That is a condition a person should look at, and it is not an outage.

Scoring

crane assess cra --json                    # the whole portfolio
crane assess cra --subject gateway --json  # one subject
crane assess cra --detail                  # per check, with the reasoning
crane assess cra --queue                   # queue it instead of running it now

Scoring stores the per-check explanation behind every number, not just the number. Nothing is approved by scoring. It is a measurement, and it never changes a decision.

Read the denominator before the score. A product with no scope decision is counted neither as passing nor as failing, and a score that does not say which population it measured is not an answer.

POST /api/v1/assess queues the same job, which is how the score moves in a deployment with no shell.

Reports

crane report list                                    # what exists
crane report leadership --format json                # for a steering committee
crane report audit --format html --out ./audit.html  # for an auditor
crane report worklist --format json                  # what to do next, by owner
crane report package --out ./handover                # hashed, for handing over

Three audiences, three documents, because a steering committee, an auditor and the people doing the work want different documents rather than different sections of one.

Reports never write to the database, so running one is always safe. Each carries the policy hash that produced its numbers and a content hash that is stable when the data has not changed, which is how you tell “nothing moved” from “nobody looked”.

Your own reports

crane report templates                    # what you have written
crane report render division-readiness --param division="Platform" --format json

A template is a Markdown file with TOML front matter in $CRANE_REPORT_TEMPLATES. Mount that directory; it is your content and is deliberately not baked into the image. Without the mount, CRAne correctly reports that you have written none.

When somebody asks for a report that does not exist, write a template rather than post-processing one that does. That way it can be run again next month.

Changing data

Everything the web interface can change is also a command, calling the same function, so the two cannot disagree about what a decision is:

crane manage create|edit|own|resolve|review|except|revoke|finding|exceptions|identity|unidentify|link

Governed acts take a reason and a name. Reference data does not, because it asserts nothing about the portfolio.

Nothing is deleted. A removed owner is rejected, a revoked exception keeps its grant. What used to be true, and when it stopped being true, remains a question you can ask.

Jobs and run directories

Every executed tool gets a directory under $CRANE_RUNS holding its stdout, stderr, artifacts and a manifest.

crane job list --status failed --json
crane job show <job-id> --json     # parameters, events, artifacts, the run directory
crane job cancel <job-id>

Where an artifact actually lives

An artifact is stored once, under $CRANE_RUNS/objects, named by its own SHA-256. The familiar copy inside the run directory is a hard link to those same bytes, so one copy carries two names and somebody debugging a failure still finds the file where they expect it. Five runs producing one 200 KB artifact occupy 195 KiB rather than 977 KiB.

This is worth knowing for two reasons. Both names have to sit on one filesystem, so $CRANE_RUNS cannot span mount points. And an artifact’s recorded URI now outlives its run directory: a pruned run no longer leaves its rows pointing at a path that is gone.

Pruning

Run directories accumulate, so something has to remove them:

crane job prune --dry-run    # what would go
crane job prune              # remove it

Failures are kept 90 days and successes 14, because a failure is the one somebody opens. Only finished runs are considered; a pending or running job still owns its directory. Nothing in the database is touched: every artifact that mattered was registered with its hash when the run finished, so what goes is the working copy and the logs. crane job show says so explicitly when a run directory has already been swept, rather than printing a path that is not there.

The bytes freed are only the bytes the disk actually gives back. A file in the run directory that another name still holds is not counted, because removing one of two names frees nothing.

The same command then sweeps the object store, removing objects no job_artifact row refers to. Pruning a run deliberately cannot free an object: an object is referenced by every run that produced those bytes, so the store needs its own sweep or it only ever grows.

What that sweep reclaims today is narrower than it sounds, and worth stating plainly: nothing yet deletes a job_artifact row, so an object stays claimed for the life of the installation. What it collects now is an object orphaned by a crash between storing one and recording it. It becomes the general thing when run retention starts removing rows.

One refusal to expect. Pointed at a database that records no job artifacts at all, the sweep stops and says so rather than treating every object as unreferenced — a typo in --database, a fresh crane init, or a restored backup is far likelier than a portfolio that genuinely produced a store and no rows. crane job prune prints objects not swept: with the reason. Run directories are still pruned normally, because that half deletes what the database lists rather than what it omits.

On a schedule

Two things run on a schedule: something feeds data in, and a worker executes queued tools. Both are safe to fire repeatedly, which is the property that makes cron usable here.

# Load whatever the collection scripts produced. Nothing happens if the batch is unchanged,
# and a batch that does not validate is rejected whole rather than half applied.
*/30 * * * *  cd /opt/crane && crane import data /var/lib/crane/incoming --json >> /var/log/crane/import.log 2>&1

# Queue validations for evidence not yet validated at this content hash.
15 * * * *    cd /opt/crane && crane validate sbom --requested-by cron --json >> /var/log/crane/validate.log 2>&1

# Do the queued work and exit. Overlapping runs are safe: a claimed job is leased.
*/5 * * * *   cd /opt/crane && crane worker run --once --json >> /var/log/crane/worker.log 2>&1

# Sweep run directories past their retention window.
30 3 * * *    cd /opt/crane && crane job prune

With the container, the same thing wrapped:

*/5 * * * * cd /opt/crane && docker compose run --rm crane worker run --once

Exit codes worth wiring into alerting: 4 from an import means the data did not validate, 5 from the worker means at least one job failed. Both want a person, neither is an outage.

Backups

The database is one file. A backup is a copy of it, and there is no built-in schedule:

30 2 * * * cp /opt/crane/data/crane.db /backup/crane-$(date +\%F).db

Copy it while nothing is writing, or use sqlite3 data/crane.db ".backup" for a consistent copy under load. Uploaded evidence under /data/uploads and generated packages under /exports are files too, and are not in the database.

So are run artifacts. $CRANE_RUNS/objects holds the stored copy of every artifact a job produced, and the database records the hash rather than the bytes — back it up and a restored database can still resolve what its rows point at. Copy it with a tool that understands hard links, or the run directories beside it turn one shared copy back into many. The rest of $CRANE_RUNS is working files and logs, and is expendable.

What is not covered yet

  • No backup policy. It is one file, so copy it on a schedule. Nothing does that for you.
  • One shared token, no accounts, no roles. Every authenticated user is the same user.
  • No secrets handling beyond the environment. A live connector’s credential belongs in an environment or a secret provider, never in the database or the image.
  • No down migrations. Restore the file you copied before upgrading.

Back to top

CRAne. The database is one SQLite file and it is yours.

This site uses Just the Docs, a documentation theme for Jekyll.