Reference¶
Public API¶
ossiq exposes a stable library interface for programmatic use. Install without extras for the core scanner (pip install ossiq); install with [cli] to also get the terminal CLI (pip install 'ossiq[cli]').
from ossiq import scan, ScanResult, ScanRecord, Settings, CVE, Package, VersionsDifference, unit_of_work
scan(uow)¶
def scan(uow: unit_of_work.AbstractProjectUnitOfWork) -> ScanResult
Runs a full dependency health scan against the project described by uow. Fetches package metadata, CVEs, and version history from the appropriate registry; runs the SAT solver to produce update recommendations; and returns a ScanResult. Must be called inside the uow context manager.
from ossiq import scan, Settings, unit_of_work
from ossiq.unit_of_work.uow_project import ProjectUnitOfWork
settings = Settings.load_from_env()
uow = ProjectUnitOfWork(settings, project_path=".")
result = scan(uow)
ScanResult¶
Aggregated output of a single scan run. Returned by scan().
Field |
Type |
Description |
|---|---|---|
|
|
Name from the project manifest |
|
|
Absolute path to the project root |
|
|
Registry used ( |
|
|
Direct production dependencies |
|
|
Dev / optional dependencies |
|
|
Indirect dependencies |
|
|
Package names where the manifest and lockfile disagree |
|
|
Cross-constraint widening opportunities (library projects only) |
ScanRecord¶
Per-package analysis record. Each entry in the ScanResult lists above is one ScanRecord.
Field |
Type |
Description |
|---|---|---|
|
|
Canonical package name |
|
|
Version currently installed |
|
|
Most recent published version |
|
|
Solver-recommended update target |
|
|
Why this version was chosen |
|
|
Days between installed and latest version |
|
|
Number of releases between installed and latest |
|
|
Semantic drift classification |
|
|
Known vulnerabilities for the installed version |
|
|
Version specifier from the manifest |
|
|
How the version was constrained (see Constraint Provenance) |
|
|
Ancestor chain for transitive packages |
|
|
How updating this package affects transitive deps |
|
|
Peer requirements the installed version fails to satisfy |
|
|
Conflicting constraints that blocked the solver |
|
|
Package URL (PURL) identifier |
|
|
SPDX license identifiers |
Settings¶
Pydantic model holding runtime configuration. Load from environment variables with Settings.load_from_env() or construct directly.
Field |
Default |
Description |
|---|---|---|
|
|
GitHub personal access token for repository enrichment |
|
|
Path to the SQLite HTTP cache |
|
|
Cache time-to-live in hours |
|
|
Emit detailed progress output |
|
|
Enable debug logging |
|
|
Treat versions published after this date as invisible |
|
|
Days a new version must age before the solver recommends it |
All fields can be set via environment variables prefixed with OSSIQ_ (e.g. OSSIQ_GITHUB_TOKEN).
CVE¶
A single vulnerability record attached to a ScanRecord.
Field |
Type |
Description |
|---|---|---|
|
|
Primary identifier (CVE, GHSA, or OSV ID) |
|
|
All aliases for this vulnerability |
|
|
|
|
|
Human-readable description |
|
|
Version strings confirmed vulnerable |
|
|
ISO 8601 publication date |
|
|
URL to the upstream advisory |
Package¶
Metadata about a package as returned by its registry.
Field |
Type |
Description |
|---|---|---|
|
|
Registry name |
|
|
Normalised name (lowercased, hyphens unified) |
|
|
Most recent stable release |
|
|
Source code repository URL |
|
|
Project homepage |
|
|
SPDX license string |
|
|
Package has been deprecated by its maintainer |
|
|
Package has been removed from the registry |
VersionsDifference¶
Semantic drift classification between two versions.
Field |
Type |
Description |
|---|---|---|
|
|
Numeric severity: 0 = no diff, 1 = patch, 2 = minor, 3 = major, 4 = build, 5 = prerelease |
|
|
Human-readable label: |
unit_of_work¶
Module alias for ossiq.unit_of_work.core. Exposes AbstractProjectUnitOfWork, the base class for the scan context. Use ossiq.unit_of_work.uow_project.ProjectUnitOfWork (the concrete implementation) to construct a scan context for a real project on disk.
from ossiq.unit_of_work.uow_project import ProjectUnitOfWork
uow = ProjectUnitOfWork(
settings=settings,
project_path="/path/to/project",
production=True, # production deps only
ignore_packages=("pytest",),
)
with uow:
result = scan(uow)
Data Model¶
The ossiq domain model is located in the ossiq.domain module. It defines the core entities used for analysis.
Project¶
A software project being analyzed. Each Project contains a name and lists of its direct production and development dependencies.
For full details, see ossiq/domain/project.py.
Package¶
A dependency of a Project. A Package is defined by its name and contains a list of all its available versions.
For full details, see ossiq/domain/package.py.
Version Models¶
The version-related models capture details from different sources and are aggregated into a single Version object.
The primary Version object aggregates package_data (from a package registry) and repository_data (from a source code repository). Other data classes like Commit and User provide granular detail about the source code history.
For a complete definition of all version-related data classes, see ossiq/domain/version.py.
Constraint Provenance¶
Most packages in a scan report were installed the normal way: a manifest declared them, the resolver picked a version, and the lockfile recorded it. The ConstraintSource field on a Dependency tracks when that was not the case — when an extra mechanism outside the normal dependency graph was controlling the version.
The five constraint types¶
Priority ordering (highest wins when multiple rules apply): OVERRIDE > ADDITIVE > PINNED > NARROWED > DECLARED.
|
What it means |
How it gets set |
|---|---|---|
|
Loose specifier in the manifest: open ( |
Default |
|
Explicit range with an upper bound in the manifest: |
Version specifier in the manifest contains an upper bound. |
|
Exactly one version allowed: |
Exact-version pin in the manifest. |
|
A separate file or setting narrowed the allowed version range without adding the package as a direct dependency. |
pip |
|
A setting forced a specific version, bypassing what the normal dependency graph would have resolved. |
npm |
Why you need to watch this¶
When a normal dependency becomes vulnerable, the fix is straightforward: update it, the resolver picks a patched version, done. Constraints and overrides break that flow. They impose version rules from outside the normal dependency graph. A constraint can pin a transitive package to a range that still contains a vulnerable version — and nothing in the lock file makes this obvious. You can stare at the lockfile, see h11==0.13.0, and have no idea that a rule somewhere else is preventing you from resolving 0.14.0.
This is the failure mode described in Against Upper-Bound Version Constraints in Libraries: once a constraint caps a package below a patched version, you cannot fix it unilaterally. The person who wrote the constraint has to release a patch first. At scale, with many transitive constraints scattered across pyproject.toml entries and nested overrides, this creates invisible debt that surfaces only when a CVE forces a full audit.
The key insight: a constraint doesn’t just describe what version is installed — it describes who has the power to change it. An OVERRIDE means someone decided this package’s own version declarations don’t matter. An ADDITIVE constraint means a separate authority is narrowing the resolution space. Both are worth tracking separately from ordinary declared dependencies.
OSS IQ surfaces constraint_info so you can see which packages are under a constraint, what kind of constraint, and which file introduced it — before a CVE forces you to find out.
Constraint provenance by package manager¶
pip classic — -c constraint files¶
pip’s -c flag in requirements.txt references a separate constraints file. Packages listed there are not installed as direct dependencies — they only narrow the version range for anything the resolver would pull in anyway.
# requirements.txt
-c constraints.txt
requests==2.31.0
When OSS IQ encounters a -c directive, it reads the referenced file and tags every package that appears in both the resolved dependencies and the constraints file with ConstraintType.ADDITIVE. The source_file field is set to the requirements.txt that introduced the -c directive. Nested -c includes are followed recursively; circular includes are detected and skipped.
A package tagged ADDITIVE in pip classic means: something outside your direct dependency list is controlling its allowed version range. If a CVE hits that package, check whether the constraint file is the thing blocking the update.
uv — constraint-dependencies and override-dependencies¶
uv exposes two settings under [tool.uv] in pyproject.toml:
constraint-dependencies— PEP 508 specifiers that narrow allowed versions without adding direct dependencies. These map toConstraintType.ADDITIVE.override-dependencies— PEP 508 specifiers that force a version regardless of what the dependency graph declares. These map toConstraintType.OVERRIDE.
# pyproject.toml
[tool.uv]
constraint-dependencies = ["h11>=0.14.0"]
override-dependencies = ["urllib3==1.26.18"]
The distinction matters: a constraint-dependencies entry cooperates with the normal resolver — it adds a lower bound, an upper bound, or an exclusion. An override-dependencies entry overrules it. If a package under override-dependencies is later found vulnerable in the forced version, no amount of updating its parents will help — the override itself is the thing to remove.
Both lists are read from pyproject.toml at scan time. Matched packages in the resolved dependency tree are tagged accordingly, with source_file set to pyproject.toml.
npm — overrides¶
npm’s overrides field in package.json forces a specific version (or range) for a matching package anywhere in the dependency tree, regardless of what each package’s own dependencies declaration says.
// package.json
{
"overrides": {
"semver": "^7.5.2",
"lodash": {
"dot-prop": "^6.0.1"
}
}
}
OSS IQ reads the overrides block from package-lock.json (where npm records the resolved overrides) and tags matching packages with ConstraintType.OVERRIDE, adding an overridden category to their categories list.
For scoped overrides — where a version is forced only when a package appears as a dependency of a specific parent — the scope_path field on ConstraintSource records the ancestor chain. In the example above, dot-prop would carry scope_path: ["lodash"], meaning the override applies only when dot-prop is pulled in by lodash. A flat override like semver has scope_path: null.
The scope_path matters for remediation: a scoped override targeting dot-prop inside lodash does not affect dot-prop when pulled in by other packages. Removing it may leave dot-prop under lodash unprotected, or free it to resolve a patched version — depending on which direction the version was being forced.
System Behavior¶
Dependency Resolution¶
Dependency Graph: The system operates on a flat list of dependencies resolved from a lockfile (e.g.,
package-lock.json).Transitive Dependencies: Transitive dependency resolution is not performed. The tool relies on the dependency resolution of the target project’s native package manager (e.g.,
npm,pip,uv).
Update Solver (plan / apply)¶
Single pass. The solver recommends versions against the current lockfile. Applying a plan re-resolves the tree, which can surface further recommendations; re-run
planuntil it reports no updates (most projects converge in one or two passes).Cooldown. Candidate versions younger than
--cooldown-perioddays (default 7) receive a heavy soft-penalty in the solver; recommendations that are still younger than the cooldown after solving are withheld into the plan’s Held for cooldown section and never applied.CVE bypass. When the installed version of a package carries a CVE, its recommendation is exempt from the cooldown hold. CVE-affected candidate versions themselves are hard-forbidden.
New transitive dependencies. Packages entering the tree for the first time are resolved by the native package manager at apply time, outside the cooldown. The plan projects their version and age and flags entries younger than the cooldown with
⚠. Under--cutoff-date, projections exclude versions published after the cutoff for deterministic time-travel runs.Forced versions (
--override pkg==version). Bypass the solver and the cooldown for one package. Persistence per ecosystem:
Ecosystem |
Direct dependency |
Transitive dependency |
|---|---|---|
npm |
specifier rewritten to the exact version |
persistent |
uv |
specifier rewritten to |
persistent |
pip classic |
constraints-file pin for the run |
constraints-file pin for the run (not persistent) |
Forced packages are reported with `ConstraintType.OVERRIDE` on subsequent scans, so they remain
visible until the override is removed.
Data Provenance¶
Package metadata is sourced from ecosystem-specific repositories (e.g., npm registry, PyPI). This is handled by a set of adapters in the ossiq.adapters module (e.g., ossiq.adapters.api_npm).
Analysis Output¶
A single analysis run produces a ProjectMetrics object.
Class: ossiq.service.project.ScanResult
Description: Contains an analysis of each dependency, including version lags, time lags, and associated vulnerabilities.
Data Sources¶
OSS IQ aggregates data from the following public sources:
Source |
Purpose |
|---|---|
Open-source vulnerability database (CVEs, security advisories) |
|
Package metadata and version history for JavaScript packages |
|
Package metadata and version history for Python packages |
|
Repository activity, releases, and maintainer signals |
Outputs¶
OSS IQ produces three categories of analysis — metrics, security, and supply chain exposure — delivered across four output formats.
Metrics¶
Each dependency produces a PackageMetrics record with the following measurements:
Metric |
Field |
Description |
|---|---|---|
Version lag |
|
Days elapsed since |
Release lag |
|
Releases between |
Drift status |
— |
Semantic classification: MAJOR, MINOR, PATCH, LATEST, or NO_DIFF |
Metrics Operationalization¶
time_lag_days and releases_lag are deterministic numbers. Teams use them to define thresholds that match their risk tolerance and enforce them automatically in CI.
A typical starting point:
Threshold |
Field |
Recommended starting value |
|---|---|---|
Maximum version age |
|
365 days |
Maximum release distance |
|
— (use |
Use the JSON export to evaluate thresholds in a CI step:
# Fail if any production package is more than 365 days behind
MAX_LAG_DAYS=365
jq --argjson max "$MAX_LAG_DAYS" \
'[.production_packages[] | select(.time_lag_days != null and .time_lag_days > $max)] | length' \
ossiq-report.json
Start with a permissive threshold to baseline your project, then tighten it incrementally as tech debt is resolved. This avoids blocking CI on day one while still creating measurable improvement targets.
For a complete GitHub Actions setup with CVE gating and outdated-package blocking, see the Version Lag and CVE Quality Gate tutorial.
Security¶
Each PackageMetrics record contains a cve array. Each entry includes:
Field |
Description |
|---|---|
|
Primary vulnerability identifier (CVE, GHSA, or OSV ID) |
|
All aliases for this vulnerability (CVE, GHSA, OSV IDs) |
|
Database that reported the vulnerability |
|
LOW, MEDIUM, HIGH, or CRITICAL |
|
Description of the vulnerability |
|
List of affected version strings |
|
Publication date (ISO 8601, nullable) |
|
URL to the upstream advisory |
Transitive CVEs. When a transitive dependency has CVEs, OSS IQ surfaces them in the transitive_packages array. The dependency_path field on each entry traces the ancestor chain from the project root to the affected package.
Supply Chain Exposure¶
OSS IQ surfaces constraint risk through the constraint_type field on each PackageMetrics record. Five tiers are recognized, ordered from highest to lowest concern:
Risk tier |
|
Signal |
|---|---|---|
Override |
|
Version forced outside the dependency graph — removing the override is the only fix |
Additive constraint |
|
A separate constraints file is narrowing the range; the constraint file owner controls the update |
Pinned version |
|
Exactly one version allowed — automatic updates are blocked |
Narrowed range |
|
An upper bound in the manifest actively excludes newer versions |
Declared |
|
Loose specifier; no constraint risk beyond normal dependency resolution |
For reports produced by OSS IQ before v1.2 (which lack a constraint_type field), the Explorer and export consumers fall back to heuristics on the version_constraint string: a bare semver (e.g. 1.2.3) is treated as PINNED; a specifier containing < is treated as NARROWED.
Output Formats¶
Console — Scan Table¶
The scan command prints two dependency tables — one for production packages, one for development — to the terminal. Each row shows package_name, CVE count, drift status, installed_version, latest_version, releases_lag, and time_lag_days.
Console — Package Detail¶
The package command prints a six-section report for a single dependency:
Drift status — version comparison with an ASCII time-lag progress bar
Dependency tree trace — ancestor path from the project root to the package
Policy compliance —
version_constraint, resolved version, and latest versionSecurity advisories — per-CVE
severity,id, source, andsummaryVia transitive dependencies — CVEs inherited from entries in
transitive_packagesLicenses — SPDX identifiers from the
licensefield
HTML Report¶
The scan --presentation html command produces a self-contained HTML file embedding an interactive Vue.js single-page application. The report includes the full dependency tables and the Transitive Dependency Explorer: an interactive D3 tree that visualises the transitive_packages dependency graph.
The Explorer supports:
Color-coded nodes by risk type — six priority tiers: CVE (red), OVERRIDE (orange dash-dot), ADDITIVE (green dotted), PINNED (orange solid-thick), NARROWED (yellow dashed), DECLARED (blue)
Fuzzy search and toggle filters (CVE, Narrowed, Override/Pinned)
Click to focus a node and highlight all ancestor and descendant paths
Alt+Click to collapse or expand a subtree
Dashed curved links between nodes sharing an identical
package_name@installed_versionZoom and pan
For full Explorer interaction details, see EXPLORER.md.
JSON Export¶
The export --output-format json command writes a single .json file conforming to export schema v1.1. The root object contains:
Key |
Contents |
|---|---|
|
|
|
|
|
Aggregate counts: packages, CVEs, outdated |
|
Array of |
|
Array of |
|
Array of |
CSV Export¶
The export --output-format csv command writes a folder named export_{project_name}/ containing three files and a Frictionless Data descriptor:
File |
Contents |
|---|---|
|
One row of project metadata and aggregate counts |
|
One row per package with all |
|
One row per CVE with all |
|
Schema references and foreign key relationships |
Versioning & Stability Guarantees¶
OSS IQ makes four commitments to users who depend on its output in CI pipelines, scripts, or downstream tooling.
Export Schema Stability¶
Each export schema version is identified by schema_version in the metadata block (e.g. "1.1"). The export --schema-version flag pins output to a specific version.
Within a schema version:
Existing fields are never renamed or removed.
New optional fields may be added — existing consumers are unaffected.
When a schema version is deprecated, the previous version remains fully supported for at least one major release cycle. Deprecation is announced in the changelog before the version is removed.
CLI Interface Stability¶
Command names, flag names, and exit codes are considered stable interfaces. Changes follow the same deprecation policy as schema versions: the old form continues to work with a deprecation warning before it is removed.
Deterministic Analysis¶
Given the same lockfile and the same version of OSS IQ, a scan always produces the same output. This makes OSS IQ safe to run as a blocking CI gate and suitable for diffing results between runs.
Note
Package registries and source code providers may remove versions or repositories at any time. OSS IQ cannot control this. Scan results may differ between runs if upstream data changes.
Note
Risk scores are time-dependent by design. The same lockfile analyzed at different points in time will produce different scores. A CVE’s risk weight increases the longer it remains unpatched (survival analysis). A new library with high release activity signals different risk than an established library with a stable, slow release cycle — and that signal shifts as the library matures.
Metric Deprecation¶
When a field or metric is deprecated, it continues to appear in exports with its original semantics until the next major schema version. Removal is always accompanied by a migration note describing the replacement field or approach.