Skip to main content

Projects

A project is the unit of source-tracked software the portal knows about. It owns scans, components, vulnerabilities, license findings, obligations, and a generated NOTICE file. Most workflows start by adding a project.

Audience

Engineers and team leads who scan their own services. Requires sign-in. The role on the project's team must be developer or higher to create / archive; team_admin to change visibility.

Anatomy of a project

FieldDescription
NameDisplay label (free text). Need not be unique — two projects in the same team may share a name; the slug is the unique key.
SlugURL-safe identifier ([a-z0-9-]+). Unique within a team — a duplicate slug is rejected with 409.
DescriptionOptional free-text summary surfaced on the project list and Overview tab.
Git URLGit URL the scan pipeline clones from. HTTPS supported. Private repos require credentials embedded in the URL — see Private repos.
Default branchThe branch the scan pipeline checks out (defaults to main). Editable from Project Settings after creation.
Visibilityteam (the only value accepted in this release — visible to members of the owning team). Set automatically on creation; mutable only via PATCH.
Owning teamThe team the project belongs to. Defaults to the team selected in the global bar; if you belong to several, the creation form lets you override it.
Owning part of the organizationOptional free text: a division, a cost centre, a squad: whatever your organization calls it. Filters the project list.
Who to askOptional free text: a name, a team alias or an address.
DistributionOptional. How the software reaches its users. See Distribution.

Organizational attributes

Two of these fields are deliberately free text. A division, a cost centre and a squad are the same slot to different organizations, and a fixed vocabulary would be wrong for most of them. Whatever you type is stored trimmed, so Platform and Platform end up as one bucket in the filter rather than two that look identical on screen.

Leave them empty and nothing changes: a project with no attributes appears in every unfiltered list, exactly as before. Set them and the project list can be narrowed to one owning unit.

Distribution

Unlike the two above, this is a fixed list, because it is not an organizational label. It is what decides which licence obligations bind: offering a network service and shipping a binary are the difference between AGPL section 13 applying and not.

ValueMeaning
Internal onlyUsed inside the organization; not conveyed to anyone outside it.
Network serviceOffered to users over a network.
BinaryShipped as a compiled artefact.
SourceShipped as source.
EmbeddedShipped inside a device.

A misspelt value is rejected rather than stored, so a typo cannot quietly become a sixth category that nothing matches.

Not decided is a valid answer, not a blank to fill. A project that has not said how it ships is judged as though it ships every way, which is the conservative reading and is what the portal already assumes. Nothing about verdicts changes when you set this field in this release; it records the fact and drives the filter.

The project list has a Distribution filter with one option that is not a stored value: Not decided, which finds the projects still to be filled in. Equality never matches an empty column, so this needs its own option, and it is the one you want while working through a portfolio.

Adding a project — UI

The Projects sidebar entry lands on the project list — every project your team memberships reach, with status badges, severity counts, an inline Scan action, and a compact per-project meta row showing n scans · m releases · last scan <relative time>:

/projects list — team-scoped table with name, last-scan status badge, severity counts, and an inline Scan action per row

The meta row sums:

  • scans — total scans this project has ever run (any status; archived runs included).
  • releases — count of release snapshots the project has accumulated (see Releases).
  • last scan — relative time since the last scan moved to a terminal status. until the first scan completes.

The list endpoint aggregates these three fields server-side in a single query, so the row is cheap to render even on portfolios of hundreds of projects.

Finding a project

The toolbar search box matches a project's name, slug, or git URL, and runs on the server against your whole portfolio — not just the projects currently on screen. Type at least part of any of the three; the term is debounced and mirrored into the URL (?search=), so a filtered list is reload-safe and link-shareable.

The page renders one server page at a time. If more projects match than fit on it, a notice above the list says how many matched and how many are shown — narrow the search to reach the rest.

  1. Sign in.

  2. Click Projects in the sidebar.

  3. Click New project in the top-right.

  4. Fill out the form:

    • Name (required)
    • Description (optional)
    • Git URL (required for source scans)
  5. Click Create.

    New project form — name, description, and Git URL fields

You land on the project's Overview tab. From here you can run the first scan — see Scans.

Project detail — Overview tab with risk gauge and quick actions

The default branch (main), visibility (team), and owning team (the team selected in the global bar) are set server-side and can be reviewed from Project Settings.

Detail-page tabs

The detail page exposes the following tabs, left-to-right:

TabWhat it shows
OverviewRisk axes (Security + License), the Build gate verdict, the Project info card (Git URL, default branch, owning team, created-at, last-scan time), and recent scans.
ReleasesSnapshots of the project at each terminal scan — the snapshot list, a "view snapshot" pin action, and per-release diff entry points. See The Releases tab.
ComponentsEvery component the scan discovered. See Components & licenses.
VulnerabilitiesOpen and triaged CVE findings. See Vulnerabilities.
LicensesThe same data viewed by SPDX identifier and tier.
ObligationsPer-component obligations + NOTICE-file generation. See Components & licenses → Obligations.
SBOMCycloneDX / SPDX exports, byte-stable. See SBOM.
ReportsGenerate-cards for NOTICE, SBOM, Vulnerability PDF, and VEX plus the project's unified download / export history. See The Reports tab.
SourceThe fetched first-party source tree from the latest succeeded scan, with file-level license findings highlighted. Sits between Reports and Remediation.
RemediationPer-component upgrade recommendations from the latest scan, including the opt-in npm remediation PR flow.
SettingsProject metadata, archive action, CI-integration helpers.
Tab order

The Source tab used to sit immediately after Licenses; it was moved to the right of Reports so the data-output cluster (SBOM / Reports / Source) is contiguous. Bookmarks and ?tab=source deep links continue to work — the slug is unchanged.

Adding a project — API

curl -sS -X POST https://trustedoss.example.com/v1/projects \
-H "Authorization: Bearer ${TRUSTEDOSS_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"team_id": "8f0c1e2a-...your team UUID...",
"name": "checkout-service",
"slug": "checkout-service",
"description": "Storefront checkout service",
"git_url": "https://github.com/acme/checkout-service.git"
}' | jq .

The response includes the project's UUID — keep it; it is the value you wire into the GitHub Action's project-id input and the GitLab CI variable.

Required fields: team_id, name, and slug. Optional: description, git_url, default_branch, and visibility. The schema rejects unknown fields (extra="forbid"), so omitting team_id or slug returns 422 (missing: body.team_id).

Finding your team_id: in the UI the project-create form has a team selector, so you never type the UUID. Over the API any signed-in user reads their teams from GET /auth/me (the auth router is mounted at /auth, not /v1) — the response carries a memberships[] array, each entry with its team_id, team_name, and your role in that team. (GET /v1/admin/teams also lists teams but is super-admin only — a team_admin calling it gets 404.) In this release the team_id field is not derived from your session — it must be in the request body.

Visibility

  • team (default and only accepted value in this release) — only members of the owning team see the project, its scans, and its findings.

Visibility is set automatically on creation. PATCH currently rejects any value other than team. Audit log records the actor on every PATCH.

See Roadmap for organization (org-wide read) availability.

Archive

  • Archive — keeps the project, its history, scans, and findings, but hides it from default lists and disables new scans. Useful when a service is retired but you still need its compliance trail.

DELETE /v1/projects/{id} performs a soft-delete (archive). The portal does not currently expose a permanent-delete operation; audit-log entries persist regardless.

The Archive action lives on Project Settings → Archive and uses an inline confirm strip to prevent accidents.

Private repositories

Source scans clone the repository from inside the worker container. Authentication option supported in this release:

  • HTTPS + Personal Access Token — set the URL to https://<token>@github.com/acme/checkout-service.git. The token is stored as part of git_url and never returned by the API in plaintext form on read endpoints.
Private repos in this release

Today the only supported credential model is HTTPS + PAT embedded in the git URL (https://<token>@github.com/acme/payment-service.git). The PAT is persisted in the project row (the API never returns it in plaintext on read endpoints, and git_url is masked in audit logs).

Implications:

  • A leaked DB snapshot still leaks every embedded PAT. Use a short-lived PAT with read-only scope.
  • SSH keys and GitHub-App installations are on the roadmap for ; rotate aggressively in the meantime.

For SSH deploy keys, see Roadmap.

The governance band

Between the project header and the tabs sits a five-tile strip that summarises where the project stands, so reading it costs no navigation while you work inside any tab:

TileWhat it says
Risk scoreThe Overview tab's overall score, from the same computation — the worse of the security and licence axes.
Build gateWhat CI would decide right now: Pass, Blocked (with the blocking counts, linking to the Vulnerabilities tab), or Never scanned.
KEV deadlinesKnown-exploited findings against their CISA remediation date, split into overdue and due within a week.
Approvals waitingComponent approvals still pending or under review for this project.
Critical trendThe critical count across the last few succeeded scans, with the change in words. Fewer than two scans draws no line — one point is not a trend.

Every number is produced by the service that owns it elsewhere in the product, so the band cannot disagree with the tab below it. Two states are deliberately distinct: a project with no successful scan shows "Never scanned" rather than a passing gate, and a band that fails to load says so instead of rendering zeros — an absent answer, not a reassuring one.

Risk score — two axes

The Overview tab now surfaces two risk axes on the project gauge instead of one composite number, so the two failure modes can be read independently:

  • Security risk — driven by the project's open vulnerability mix. The band (Critical / High / Medium / Low / Info) is set by the most severe open finding; within the band the score scales as n / (n + 4) (non-saturating, so the band itself is the primary signal — adding more findings cannot bump you up a band).
  • License risk — driven by the project's license-tier mix. Forbidden licenses dominate the band; Conditional rows raise the score within the band but never promote it to Critical on their own (the previous "any conditional component = Risk 100" behaviour was removed in W1).

The legacy single risk_score field is still exposed on the API as max(security_axis, license_axis) for back-compat with the build gate and CI integrations; the UI uses the two-axis breakdown.

Both axes refresh after every scan and after every CVE re-detection. Read them as relative indicators across your portfolio, not absolute SLAs — drilling into the project shows the per-axis breakdown.

Old "single risk gauge" screenshots

Screenshots taken before W1 show a single gauge labelled "Risk". The two-axis card replaces it. The numbers are not strictly comparable between the old single score and either of the two new axes — re-baseline against your portfolio after the upgrade.

Build gate verdict (Overview tab)

The Overview tab shows a Build gate card next to the risk gauge. It surfaces the same build-blocking verdict the CI integration computes — so you can read the gate result in the portal without opening a CI log. The card evaluates the project's main line — the newest successful scan whose branch matches the project's default branch (falling back to main, and to the newest successful scan of any branch if neither matched). A project scanned on several branches therefore shows a stable verdict instead of one that flips to whichever branch finished last.

Over the API, a CI job should name its own branch: GET /v1/projects/{id}/gate-result?ref=<branch> evaluates that branch alone, so a release branch's critical CVE cannot block main's pipeline. Fully-qualified refs (refs/heads/main, $GITHUB_REF) are normalized the same way scan triggers normalize them. A branch with no successful scan returns the no-signal pass rather than borrowing another branch's findings.

The build gate (also called the policy gate) is the CI-blocking mechanism that exits non-zero when a build carries critical CVEs or forbidden-tier licenses. The concept and how to wire it into a pipeline live in GitHub Actions → the build gate and Glossary → Build gates; this card is the read-only, in-UI view of the same verdict.

The card shows:

ElementMeaning
Pass / Fail badgePass (green, shield-check) when the latest successful scan has no critical CVEs and no forbidden licenses; Fail (red, shield-x) otherwise.
ReasonOn Fail, a one-line explanation of what tripped the gate.
Critical CVEsCount of open critical-severity findings on the evaluated scan. Open = status not in not_affected, fixed, false_positive.
Forbidden licensesCount of distinct components carrying at least one forbidden-tier license.
EPSS ≥ {threshold}Shown only when an operator has enabled the EPSS gate (GATE_EPSS_THRESHOLD set on the portal). Count of open findings whose EPSS score meets or exceeds the threshold. Hidden when the EPSS gate is disabled (the default). See Gate the build on EPSS.
No scan yet

A project that has never had a successful scan shows a neutral No scan yet state instead of a green pass — there is nothing to evaluate. Run a scan (see Scans) and the card fills in.

CVE — Common Vulnerabilities and Exposures; EPSS — Exploit Prediction Scoring System. See the Glossary for both.

The card is read-only — it reflects the verdict but does not change policy. The thresholds (severity floor, EPSS) are operator- and CI-side settings; see GitHub Actions and GATE_EPSS_THRESHOLD.

Project info (Overview tab)

The Overview tab has a Project info card next to the risk gauges. It collapses the project's identifying metadata into one read-only block, so you do not have to open Settings for a quick lookup:

FieldSourceNotes
Git URLProject git_url.Click-to-copy. The URL is masked when a personal-access token is embedded (the token segment is rendered as ***); the raw value never appears on read endpoints.
Default branchProject default_branch.Editable from Settings.
Owning teamProject team.Links to the team's /admin/teams/{id} admin view when the viewer is super_admin.
CreatedProject created_at.Absolute timestamp on hover, relative time on the face.
Last scanProject last_scan_at (the same value the project list aggregates). until the first scan reaches a terminal status.

The card is the same data the project-list meta row exposes, surfaced once at the top of the detail page so the reader does not have to bounce back to the list to remember the project's repo URL.

The Releases tab

Project, scan, release, version, branch

Four words, and they are not interchangeable. A project is what you manage — a repository or image. A scan is one analysis run of it, with a status: queued, running, succeeded, failed, cancelled. A release is a succeeded scan read as a point-in-time record; every succeeded scan is one release, which is why the Releases tab lists more rows than you have shipped builds. A version (v1.2.3) is an optional label you attach when triggering a scan, naming the unit you intend to ship — it is a property of a release, not a separate thing, and a labelled release is exempt from retention. A branch is the line of development a scan targeted; unlike a version it keeps moving, and newer scans on the same branch supersede older ones.

Every time a scan reaches a terminal succeeded status the portal records a release snapshot — an immutable point-in-time view of the project (component list, license tier mix, vulnerability findings, scan id) tagged with the scan's completion timestamp. The Releases tab on a project lists those snapshots newest-first:

ColumnWhat it shows
SnapshotThe scan completion time (yyyy-mm-dd HH:MM) + relative time.
BranchThe normalized git ref the scan ran against (main, pr-12), or ad-hoc when the trigger carried no ref.
Scan kindsource or container.
Severity countsCritical / High / Medium / Low at snapshot time.
License mixAllowed / Conditional / Forbidden bars at snapshot time.
ActionsView components (jumps to the Components tab pinned to that scan) and View snapshot (pins ?scan=<id> and reloads the Overview with the snapshot's data).

Click a release row directly to navigate to the Components tab with the snapshot pinned. The pin propagates as a ?scan=<id> URL parameter so the deep link survives reload and can be shared with a teammate — every tab on the project (Components, Vulnerabilities, Licenses, …) reads from the pinned snapshot until you clear the pin in the breadcrumb. Removing the pin restores the latest succeeded scan as the data anchor everywhere.

Downloads follow the pin too. With a release pinned, the NOTICE you download is composed from that release's licenses, not from the newest scan — so the attribution document you shipped with an earlier release stays retrievable after later scans succeed. Over the API this is GET /v1/projects/{id}/notice?scan_id=<id>; as with every other snapshot-anchored read, an id that is not one of this project's succeeded scans returns 404.

Looking a version up by name

Over the API, GET /v1/projects/{id}/releases?release=4.0 returns the snapshot carrying that version label. Because a labelled scan supersedes any earlier scan claiming the same label, the answer is at most one row — read its scan_id and pin it on the detail endpoints (?scan_id=) to read that release's components, licences, findings, SBOM, or NOTICE. An unknown label returns an empty list rather than a 404: not having shipped 9.9 yet is a normal answer, and a 404 would be indistinguishable from "no such project".

The match is exact, not a search — 4.0 does not find 4.0.1. Surrounding whitespace is trimmed on both sides.

Every detail read also takes the label directly, so you rarely need the two-step. GET /v1/projects/{id}/notice?release=4.0, .../overview?release=4.0, .../vulnerabilities?release=4.0 and the rest read as of that version — a permanent URL you can put in a ticket, because the label follows the snapshot that currently holds it. Pass ?scan_id= instead to pin one immutable scan; if you pass both, scan_id wins. An unknown label returns 404, the same response an unusable scan_id gets, so neither reveals whether the other exists.

Filtering by branch

Both GET /v1/projects/{id}/releases?ref=main and GET /v1/projects/{id}/scans?ref=main narrow to one branch. A bare branch and a fully-qualified ref (refs/heads/main, refs/pull/12/merge) both work — they are normalized the same way scan triggers normalize them, so a branch reaches its own rows either way.

The two lists answer different questions. Releases covers succeeded scans only, so it is the branch's usable history; the scans list covers every status, so it is where a branch's failed attempts stay visible.

Branch and version are separate axes and neither replaces the other. A branch keeps moving — the newest scan on it supersedes the previous one — while a version names a shipped unit and is displaced only by another scan claiming that same version.

The companion Compare screen (the Compare button on the Releases-tab toolbar, enabled once the project has at least two releases) takes two snapshot ids — a base and a target — and shows what changed between them: added / removed / version-changed components, introduced / resolved vulnerabilities, and the risk-score, per-severity, license-tier, and build-gate deltas. A swap control flips base and target. The button defaults to comparing the newest release (target) against the one before it (base), and the two ids live in the URL (?base=&target=) so a specific comparison can be shared. It is the canonical diff view for "what changed between release X and release Y".

The Reports tab

The Reports tab is a single landing page that unifies the project's downloadable artifacts:

  • Four generate cards for NOTICE, SBOM, Vulnerability PDF, and VEX. Each card is a deep link — clicking the card's action button switches to the relevant domain tab (Obligations / SBOM / Vulnerabilities / Vulnerabilities) where the actual format chooser and download buttons live. The currently pinned ?scan= snapshot is preserved across the jump.
  • An export history table on the right, with columns When (relative + absolute timestamp), Who (the actor's email — for anonymized rows kept for audit), Type (one of NOTICE / SBOM / Vulnerability PDF / VEX), Format (the exact format string — cyclonedx-json, spdx-tv, openvex, etc.), Scan (first eight characters of the scan id), and Size (humanized; when the renderer did not record a size).
  • A toolbar Type multi-select filter and Prev / Next pagination, both mirrored to the URL as ?rpt_type=<type> / ?rpt_page=<n> so a filtered view is reload-safe and link-shareable.

Authorisation is the same posture as the SBOM and PDF exports — any team member with at least developer can read history; non-members receive 404 (existence-hide). The history table is append-only: there is no edit, delete, or replay action. To re-download an artifact, click the relevant generate card and re-export it from the domain tab.

The Reports tab does not duplicate the domain-tab download UX

Generate cards always deep-link to the domain tab (Obligations, SBOM, Vulnerabilities) rather than spawning a generation dialog inside the Reports tab. The intent is to keep one canonical UX per format and avoid drift between two download surfaces. The added value of the tab is the history view across all formats in one place.

Verify it worked

After creating a project:

  1. The project appears in Projects with status Idle (no scans yet).
  1. The Overview tab shows zero components and zero vulnerabilities.

    -- components / vulnerabilities are scan-scoped, so a project
    -- without a scan necessarily shows zero of both
    SELECT count(*) FROM projects p
    WHERE p.name = 'docs-uat-new-project'
    AND NOT EXISTS (SELECT 1 FROM scans s WHERE s.project_id = p.id);
  1. The audit log (/admin/audit, super-admin only) records target_table=projects&action=create with your user_id.

    SELECT count(*) FROM audit_logs
    WHERE target_table = 'projects'
    AND action = 'create'
    AND actor_user_id IS NOT NULL
    AND created_at > now() - interval '1 hour';

Troubleshooting

"Repository URL is invalid"

The wizard validates the URL must start with http:// or https:// (HTTPS strongly preferred). git@… and ssh://… URLs are not accepted by the form in this release; use the HTTPS clone URL. The portal does not verify reachability — that happens at scan time. If the URL is rejected at form submission, double-check for typos.

"Project slug already in use"

The slug (not the name) is unique per team — the 409 fires when another project in the same team already uses that slug. Names may repeat. Pick a different slug, e.g. checkout-service-legacy.

Forbidden when creating a project

Your role on the owning team is below developer. Ask a team admin to invite you with the right role — see Users & teams.

Roadmap

Items the manual previously promised that are not in this release; tracked for later releases.

  • Project tags for portfolio grouping — planned.
  • organization (org-wide) visibility — planned.
  • SSH deploy-key generation from Project Settings — planned.
  • Permanent project delete with typed-name confirmation — under design; soft-delete (archive) is currently the only option.
  • SSH (git@…, ssh://…) URL acceptance in the create wizard — planned.

See also