Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

All notable changes to Traust are documented here.

## [0.44.0]

- Restore `portfolio inventory-export --format csv` through a registered Core Insight view.
- Preserve the eight-column JSON-array stdout, newest-first registration order, current
owners, deduplicated shipping products and product-definition contact semantics.
- Keep per-invocation `--pd-cache-dir` overrides and the no-files default; expose canonical
inventory JSON plus Core-owned Markdown/HTML/CSV through `dashboard view inventory-export`.
- Replace client-side inventory joins with bounded, batched Core reads. Refuse unsupported
scopes and over-budget workloads rather than silently ignoring scope or truncating rows.
- Reuse Core's shared CSV and human renderers to keep untrusted source text inert.

## [0.42.0]

- Publish the ranked PQC worklist as a Core-owned Insight report while preserving
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.42.0
0.44.0
29 changes: 21 additions & 8 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -456,18 +456,31 @@ judgements on existing core findings; it does not replace machine-report intake.
```bash
traust portfolio onboard --product example --label Example --org example-org
traust portfolio owner-hints --repo https://github.com/example-org/api
traust portfolio inventory-export [--pd-cache-dir <feeds-dir>]
traust portfolio inventory-export [--format json|csv] [--pd-cache-dir <feeds-dir>]
traust products add example --label Example --ownership owned --business-unit Commerce
traust products list
```

`inventory-export` composes every registered repository from the portfolio's own lists:
`Repository`, `URL`, `Host`, `Organization/Group`, `Repo Name`, `Owner Team` (current owning
teams), `Products` (products shipping it) and `Accountable Contacts` (product-definitions
candidates for its URL through the recorded product map, with source field and embargo
eligibility), as JSON (no CSV until core registers its Insight view (a priority follow-up)). `--pd-cache-dir` reads a
product-definitions snapshot other than `locations.feeds_cache` for that run only. A contact
is a candidate, never an owner, and grants no authority to disclose findings. Declared
`inventory-export` publishes Core's global inventory Insight view: every registered
repository in newest-first registration order, with `Repository`, `URL`, `Host`,
`Organization/Group`, `Repo Name`, `Owner Team` (current owning teams), `Products`
(products shipping it) and `Accountable Contacts` (product-definition candidates for
its URL through the recorded product map, with source field and embargo eligibility).
Default stdout remains the same JSON array with raw values; `--format csv` emits Core's
eight-column CSV instead, joining list cells with semicolons and neutralizing spreadsheet
formulas and forged rows through the shared renderer. Neither form creates report files.

`traust dashboard view inventory-export --out <directory>` writes Core's Markdown, HTML
and CSV plus the canonical `{rows: [...]}` JSON wrapper. This view takes no product, team
or historical scope; unsupported options are refused. Reads use a cumulative 10,000-record <!-- estate-data-ok: Core's public workload ceiling, not a deployment inventory count -->
workload budget across inventory, related rows, mappings and product-definition work;
overflow fails rather than exporting a partial inventory. Building the report scans
nothing and changes no database rows or object-store bytes.

`--pd-cache-dir` selects a different cached product-definition snapshot for this invocation
only. Missing optional cache data retains empty contacts, not a claim that no accountable
party exists. A contact is a candidate, never an owner, and grants no authority to disclose
findings. Declared
per-repository inventory facts (service labels, managers, tracker routes, escalation
contacts) are retired: traust-core has one ownership source, the onboarding and owner-import
doors. `owner-hints` prints forge hints; it never stores them. Product boundary caveats are
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "traust"
# version must equal the VERSION file — check_docs_consistency.py
# version-sync check fails the pre-push when they drift (and fails
# closed if this field is removed)
version = "0.42.0"
version = "0.44.0"
license = "Apache-2.0"
description = "AI agent tooling for automated security audits of Red Hat Hybrid Platforms source code"
requires-python = ">=3.11"
Expand All @@ -32,7 +32,7 @@ traust = "traust.cli.__main__:main"
dev = ["ruff>=0.6", "pytest-xdist>=3", "psycopg[binary]>=3.1"]

[tool.uv.sources]
traust-core = { git = "https://github.com/traust-security/traust-core.git", rev = "374341a673dea75cd92035b161a9b4be97edddee" }
traust-core = { git = "https://github.com/traust-security/traust-core.git", rev = "81e27c598e602594ae517aa2e0fc39e49637f962" }

# Multi-skill CLIs live under src/traust/; harnessing/<skill>/ for single-skill
# scripts. Install for deps + importable path helpers.
Expand Down
4 changes: 4 additions & 0 deletions src/traust/cli/dashboard_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ def main(argv: list[str] | None = None) -> int:
_scope_flags(ap)
ap.add_argument("--out", type=Path, required=True, help="the folder the view is written to")
args = ap.parse_args(argv)
if args.view == ViewName.INVENTORY_EXPORT:
for flag in ("until", "sla_policy", "invoice"):
if getattr(args, flag) is not None:
ap.error(f"--{flag.replace('_', '-')} is not supported by inventory-export")
if args.panel and args.view != ViewName.EXECUTIVE_SUMMARY:
ap.error("--panel requires the executive-summary view")

Expand Down
78 changes: 15 additions & 63 deletions src/traust/cli/portfolio_onboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,20 @@
import argparse
import re
from pathlib import Path
from typing import Any
from urllib.parse import urlsplit
from typing import Any, cast

import pydantic
import yaml
from traust_core.v1.context import Context
from traust_core.v1.errors import NotFoundError, ValidationError
from traust_core.v1.models.base import Dto, Model
from traust_core.v1.models.common import every_item
from traust_core.v1.models.insight import InventoryExport, ViewFilter, ViewName
from traust_core.v1.models.inventory import (
OrgName,
OwnerAssignment,
OwnershipSource,
RefName,
ReleaseCommit,
RepositoryFilter,
TeamName,
VersionLabel,
)
Expand Down Expand Up @@ -244,74 +242,28 @@ def release(version: str) -> Any:
return run(call)


INVENTORY_COLUMNS = (
"Repository",
"URL",
"Host",
"Organization/Group",
"Repo Name",
"Owner Team",
"Products",
"Accountable Contacts",
)


def inventory_export(argv: list[str] | None = None) -> int:
ap = parser("portfolio", "inventory-export", "Joined inventory, owners and contacts.")
ap.add_argument("--format", choices=("json", "csv"), default="json")
ap.add_argument(
"--pd-cache-dir",
type=Path,
help="product-definitions snapshot for contacts (default: locations.feeds_cache)",
)
args = ap.parse_args(argv)

def call() -> Any:
def call() -> object:
core = load(args.config_home)
rows = _inventory_rows(core, args.pd_cache_dir)
return [dict(zip(INVENTORY_COLUMNS, row, strict=True)) for row in rows]
if args.pd_cache_dir is not None:
locations = core.config.locations.model_copy(
update={"feeds_cache": str(args.pd_cache_dir.resolve())}
)
core.config = core.config.model_copy(update={"locations": locations})
rendered = core.services().insight.publish(ViewName.INVENTORY_EXPORT, ViewFilter())
if args.format == "csv":
assert rendered.csv is not None
return rendered.csv.rstrip("\r\n")
model = cast(InventoryExport, rendered.model)
return [row.model_dump(mode="json") for row in model.rows]

return run(call)


def _inventory_rows(core: Context, cache_dir: Path | None) -> list[tuple[object, ...]]:
"""Every registered repository with its current owning teams, the products shipping it and
the product-definitions contacts for its URL: composed from the portfolio's own lists."""
portfolio = core.services().portfolio
repositories = list(
every_item(lambda page: portfolio.list_repositories(RepositoryFilter(), page))
)
resolution = portfolio.resolve_products(
core.product_definitions(cache_dir).read(),
portfolio.product_mappings(),
repo_urls=[repo.url for repo in repositories],
)
contacts = {
match.query.value: [
f"{c.kerberos_id} ({c.field}; embargo_cleared={c.embargo_cleared})"
for c in match.contacts
]
for match in resolution.results
if match.query is not None
}
rows = []
# NEED(core: bulk owners/products for inventory export): PortfolioService at the pinned core
# answers owners (`list_owners`) and shipping products (`list_products_of`) one repository at
# a time, with no list or by-ids variant, so this export makes two store queries per
# repository until core has one.
for repo in repositories:
assert repo.id is not None
parts = urlsplit(repo.url)
organization, _, name = parts.path.strip("/").rpartition("/")
rows.append(
(
f"{organization}/{name}" if organization else name,
repo.url,
parts.hostname or "",
organization,
name,
[team.name for team in portfolio.list_owners(repo.id)],
[str(product.slug) for product in portfolio.list_products_of(repo.id)],
contacts.get(repo.url, []),
)
)
return rows
5 changes: 4 additions & 1 deletion tests/test_cloud_config_doors.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
import yaml
from traust_core.v1 import contracts
from traust_core.v1.models.identity import IdentitySource, Principal
from traust_core.v1.models.inventory import RefName
from traust_core.v1.models.operations import OrderRequest, WorkerId
from traust_core.v1.models.values import HttpsRepoUrl
from traust_core.v1.models.values import GitSha, HttpsRepoUrl
from traust_core.v1.models.vocabulary import ActorKind
from traust_core.v1.repositories import apply_schema, create_database_engine
from traust_core.v1.repositories.sql import Dialect
Expand Down Expand Up @@ -355,6 +356,8 @@ def test_record_binds_the_audit_to_its_leased_order(home: Path, tmp_path: Path)
repo = services.portfolio.register_repository(
HttpsRepoUrl.parse(REPO, load(home).config.forge_hosts), False
)
assert repo.id is not None
services.portfolio.mark_default_branch(repo.id, GitSha.parse(SHA), RefName("main"))
operator = Principal(
identity="operator@example.com",
kind=ActorKind.HUMAN,
Expand Down
Loading
Loading