linceo
A DevSecOps tool orchestrator: it runs one or more security tools against a
workspace, normalizes their output into a single Finding model, and
produces one verdict — one exit code — for the entire run, instead of one
report per tool that a pipeline has to reconcile by hand.
The full architecture and scope of the current version are recorded in
docs/adr/ADR-000-arquitectura-base-y-alcance-v0.1.md.
That document is the contract for this project; anything below is a summary
of it, not a replacement.
Status
Pre-release (0.1.0.dev0). The engine, both v0.1 reference tool
integrations (Gitleaks, Trivy), the reference context providers (local,
azure_devops, github_actions), console/JSON/SARIF reporting, the
doctor command, and the reference container image are in place. See the
ADR's build order for what ships next.
Non-negotiable constraints
- Runs the same way regardless of which CI platform invokes it.
- The happy path makes no network call.
- No shared state or backend of its own: one run is one self-contained execution.
- The container image is the primary distribution unit; PyPI supports local development, bring-your-own tool binaries.
- No client-specific configuration ever lives in this repository.
Installation
Container image (primary distribution, ADR §4/R4)
The container image is the unit of compatibility between the orchestrator
and the exact tool versions it invokes: it bundles pinned, checksum-verified
builds of Gitleaks and Trivy, Checkov in its own isolated environment
(ADR §10, §8.3), plus Trivy's vulnerability database pre-fetched at build
time, so a scan runs fully offline by default. It also installs the
linceo[remote-config] extra (see "Remote policy" below) — being the
primary distribution vehicle (ADR R4) is exactly why remote policy
resolution, a pipeline feature, needs to work out of the box here;
pip install linceo on its own still gains no HTTP client at all (ADR R2).
Pull a published release — built and pushed by
.github/workflows/release.yml, no
authentication needed (see docs/RELEASING.md):
docker pull ghcr.io/jdiegoisaza/linceo:latest # or a specific version, e.g. :0.2.0
The image is large — about 2.37 GB — because of what R2 (offline by
default) actually costs, not because of bloat. Measured breakdown (docker history, ADR §10 amendment for the full analysis):
| Layer | Size | What it is |
|---|---|---|
| Trivy's vulnerability database | 1.4 GB | Baked in at build time so a scan never touches the network (ADR §5). Most of it is OS-package coverage trivy fs (the only mode this project uses, ADR §10) never queries — see the ADR amendment for why that isn't easily trimmed. |
| Checkov's isolated venv | 196 MB | Its graph-analysis engine and AWS IAM-policy checks — used by the frameworks this integration keeps, not bloat from the ones it skips. |
| Gitleaks + Trivy binaries | 190 MB | The two Go binaries themselves. |
git (+ its Perl dependency) |
~114 MB | Debian's git package hard-depends on Perl; there is no lighter split package. |
Expect that on the first docker pull, not a slow subsequent one — it's
worth knowing before you budget CI cache space or a cold-start pull, not
after.
Run it against a workspace by mounting it and appending a subcommand —
exactly as you would to the linceo binary itself:
docker run --rm -v "$PWD:/workspace" ghcr.io/jdiegoisaza/linceo scan secrets
docker run --rm -v "$PWD:/workspace" ghcr.io/jdiegoisaza/linceo scan sca --fail-on high
docker run --rm ghcr.io/jdiegoisaza/linceo doctor
docker run --rm ghcr.io/jdiegoisaza/linceo alone (no subcommand) prints
--help. The image runs as a fixed non-root user (uid/gid 1000); if the
mounted workspace's files are owned by a different uid on the host, add
--user "$(id -u):$(id -g)" to the docker run invocation.
To build it yourself instead — for local development, or to reproduce a published image bit-for-bit from source — from the repository root:
docker build \
--build-arg BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--build-arg VCS_REF="$(git rev-parse HEAD)" \
-t linceo:local .
(swap ghcr.io/jdiegoisaza/linceo for linceo:local in the docker run
examples above). Omitting LINCEO_VERSION here is expected for a local
build — it's the release workflow that passes the real one; see
docs/RELEASING.md, "Container image".
Which exact versions of Gitleaks and Trivy — and how old its vulnerability
database is — a given image carries is queryable two ways, and both
describe the same pinned reality (see Dockerfile and
src/linceo/cli/doctor.py):
- Without starting the container:
docker inspect ghcr.io/jdiegoisaza/linceo:latest(orlinceo:local, orskopeo inspectagainst any pushed image) shows the OCI labels the build embeds —dev.linceo.tool.gitleaks.version,dev.linceo.tool.trivy.version,dev.linceo.trivy-db.built-at, plus the standardorg.opencontainers.image.*set. - From inside it:
linceo doctor(see below) — the same command works identically for apip installed, bring-your-own-tool setup.
Context providers inside the container: which environment variables cross the boundary
docker run does not inherit the invoking process's environment — nothing
reaches linceo inside the container unless passed explicitly with -e VAR (or --env-file). Which variables matter depends on --platform (or
its auto detection, ADR §4 R1):
-
local(the default outside any recognized CI platform) needs nothing extra: it resolves everything from the mounted git checkout itself (git rev-parse,git remote get-url origin) — no environment variable read at all. -
azure_devopsneeds the Azure Pipelines agent's own variables forwarded explicitly — all standard, non-secret predefined variables (canonical list:src/linceo/providers/azure_devops.py,ENV_VARS, plus theauto-detection sentinel below):Variable Required? Resolves TF_BUILD— autodetection's own sentinel (linceo.providers.detection)BUILD_REPOSITORY_NAMErequired repositoryBUILD_SOURCEVERSIONrequired commitBUILD_SOURCEBRANCHoptional branch(non-PR trigger)SYSTEM_PULLREQUEST_SOURCEBRANCHoptional branch(PR trigger)SYSTEM_PULLREQUEST_PULLREQUESTIDoptional pull_request_id(Azure DevOps' own internal id)SYSTEM_PULLREQUEST_PULLREQUESTNUMBERoptional pull_request_id(GitHub-backed repository, human-facing number — preferred over the internal id when both are set)BUILD_BUILDIDoptional build_idBUILD_REPOSITORY_URIoptional source_urldocker run --rm \ -e TF_BUILD -e BUILD_REPOSITORY_NAME -e BUILD_SOURCEVERSION \ -e BUILD_SOURCEBRANCH -e SYSTEM_PULLREQUEST_SOURCEBRANCH \ -e SYSTEM_PULLREQUEST_PULLREQUESTID -e SYSTEM_PULLREQUEST_PULLREQUESTNUMBER \ -e BUILD_BUILDID -e BUILD_REPOSITORY_URI \ -v "$PWD:/workspace" ghcr.io/jdiegoisaza/linceo scan secrets
The reference
azure-pipelines/templates/linceo-scan.ymlalready does this by default (seedocs/ADOPTION.md) — nothing to configure if you use it. The snippet above only matters if you invoke the image directly, outside that template.Deliberately an explicit allowlist, never the whole environment: a real Azure Pipelines job's process environment routinely carries far more than this — feed credentials, mapped secret variables, other steps' exports — none of which
linceohas any business seeing inside the container.Diagnosing this from inside a container:
docker run --rm -e TF_BUILD -e BUILD_REPOSITORY_NAME ... ghcr.io/jdiegoisaza/linceo contextshows exactly which platform got selected, why, and the value (or absence) of every variable above — run it before a real scan ifautoseems to be picking the wrong platform. -
github_actionsneeds the runner's own default variables forwarded explicitly — every one this project reads (canonical list:src/linceo/providers/github_actions.py,ENV_VARS, plus theauto-detection sentinel below):Variable Required? Resolves GITHUB_ACTIONS— autodetection's own sentinel (linceo.providers.detection)GITHUB_REPOSITORYrequired repositoryGITHUB_SHArequired commit— the ephemeral merge commit GitHub Actions creates on apull_requesttrigger, used as-is (see the module's docstring for why)GITHUB_REFoptional branch/tag (non-PR trigger)GITHUB_HEAD_REFoptional branch(PR trigger)GITHUB_RUN_IDoptional build_idGITHUB_SERVER_URLoptional source_url(combined withGITHUB_REPOSITORY)docker run --rm \ -e GITHUB_ACTIONS -e GITHUB_REPOSITORY -e GITHUB_SHA -e GITHUB_REF \ -e GITHUB_HEAD_REF -e GITHUB_RUN_ID -e GITHUB_SERVER_URL \ -v "$PWD:/workspace" ghcr.io/jdiegoisaza/linceo scan secrets
These are GitHub Actions' own default environment variables — every job already has them, with nothing to configure, unlike Azure Pipelines' agent-specific predefined variables above; the snippet only matters when invoking the image directly inside a container-based step.
Diagnosing this from inside a container:
docker run --rm -e GITHUB_ACTIONS -e GITHUB_REPOSITORY ... ghcr.io/jdiegoisaza/linceo contextshows the same breakdown as forazure_devopsabove.
pip install (local development, bring-your-own tool binaries)
pip install linceo
Requires Python 3.11 or later. Gitleaks and Trivy are not bundled via
PyPI — install them yourself and run linceo doctor to confirm each one
is on PATH, at a compatible version, and (for Trivy) how old its
vulnerability database is; a missing or incompatible tool gets an
actionable install/upgrade hint printed right there.
Remote policy (optional, ADR R2, §8.4)
Thresholds, per-tool configuration, and the report banner (see below) can
be governed centrally instead of per-repository — the case where security
maintains one policy document and every pipeline inherits it. Declare it
in the scanned repository's own .devsecops/config.toml (never in this
repository, R5):
[remote_policy]
repository = "security-baseline" # name only, never a URL
# path, project, token_env are all optional — see the ADR amendment for defaults
Fetching one requires the linceo[remote-config] extra — already bundled
in the reference container image (ADR R4: the primary distribution
vehicle, and remote policy resolution is a pipeline feature). Only a
pip installed linceo needs it added explicitly:
pip install 'linceo[remote-config]'
Today's one reference source is Azure DevOps
(linceo.providers.azure_devops.AzureDevOpsPolicySource): it resolves the
organization and project from the same Azure Pipelines variables
azure_devops context resolution already reads
(SYSTEM_COLLECTIONURI/SYSTEM_TEAMPROJECT), and authenticates via a
bearer token read from the environment variable token_env names — never a
value in the file itself (ADR §9). token_env defaults to
SYSTEM_ACCESSTOKEN — the running build's own OAuth identity, which the
reference azure-pipelines/templates/linceo-scan.yml already maps and
forwards by default, so the common case (a policy repository in the same
organization) needs nothing declared here at all; a repository outside
that organization needs a Personal Access Token instead, via a custom
token_env and the template's own policyRepoToken parameter — see
docs/ADOPTION.md, "Fuente remota de la política", for both modes with
examples. [[exclusions]], [[skipped_tools]], and [[severity_overrides]]
stay local-only always; a remote document declaring any of the three is a
configuration error, not a silently-ignored one.
A failed fetch never fails a run: it falls back to the last successfully
fetched copy (cached under ~/.cache/linceo/remote-policy by default, or
$LINCEO_POLICY_CACHE_DIR, owner-only permissions, keyed by the source's
own resolved organization/project/repository/path so two tenants of a
shared runner never collide), or to the local document alone if there is no
cache yet — always declared prominently in the report, with the cached
copy's own age, the same stale_data principle §5 already applies to a
tool's own vulnerability database. That fallback only ever has something to
use if the cache directory itself survives between runs — the reference
template mounts a persistent one by default (docs/ADOPTION.md, "La caché
local, y por qué la plantilla monta un volumen para ella"); a bare
docker run --rm with no such mount starts every run with an empty cache.
A 404 from Azure DevOps is reported with all four of its possible causes
named explicitly (repository, path, branch, permissions — see the same
doc, "Diagnosticando un 404"), never just the bare HTTP status. The bearer
token itself is never logged,
cached, or otherwise persisted (ADR §9).
Report banner (ADR §7, §8.4)
The console report opens with a one-line ASCII box naming the product —
"linceo" unless a policy document overrides it:
banner = "ACME Corp Security Gate"
Governed, like [thresholds]/[tool_defaults]: it can be set locally, or
centrally in a remote policy document (see above), where it replaces the
local one entirely for every pipeline that inherits it — never a
per-invocation CLI flag or environment variable, since a name an
organization picks a few times a year has no use for one. Constrained to
printable ASCII (0x20-0x7E) and at most 72 characters — no control
characters, no ANSI escape sequences, no embedded newline, since an
org-wide remote document's content reaches every consuming pipeline's own
terminal verbatim. An invalid local banner is a configuration error like
any other; an invalid remote one degrades to the cached or local banner
instead of failing every pipeline in the organization over a cosmetic
mistake — see the ADR amendment for the full reasoning behind that
asymmetry. Console-only: --max-rows's own reasoning applies here too, so
neither the JSON report nor SARIF ever carries it.
Development
uv sync
uv run linceo --version
See CONTRIBUTING.md for the full set of task commands,
and AGENTS.md for repository conventions and hard rules.
License
Metadata
Release files for linceo 0.9.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| linceo-0.9.1.tar.gz | 468.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| linceo-0.9.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 672.6 kB
Release files / linceo-0.9.1.tar.gz
| Download URL | linceo-0.9.1.tar.gz |
|---|---|
| Size | 468.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9ea2f3337229e7bff8a2a8b3e84006cbd70d81fd394112c4a621f3eb58bbafa6
|
|
BLAKE2b-256 checksum How to use checksums |
b37960464603d26b8a626f53f63743d01363ad7d60ce4ee59f099576bc6070c6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency logRelease files / linceo-0.9.1-py3-none-any.whl
| Download URL | linceo-0.9.1-py3-none-any.whl |
|---|---|
| Size | 203.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e38f2cdcee35c84bf3227d2cb925fd51f6a01a8a48fc1e562f03210c8c3f76c2
|
|
BLAKE2b-256 checksum How to use checksums |
37f4bd1ab083fe47350f5148ba42a9e5ba6fd8833ef5c5485396e149c25080ad
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency log