feat(security): ingest gitea backup assets

This commit is contained in:
ogt
2026-07-14 14:16:54 +08:00
parent 69d2e4381d
commit f963452b3e
2 changed files with 213 additions and 0 deletions

View File

@@ -210,6 +210,7 @@ async def scan_once(
"database_catalog",
"ai_catalog",
"model_registry",
"gitea_bundle_backup",
),
scan_depth: str = "shallow",
) -> str:
@@ -961,6 +962,19 @@ async def _collect_runtime_assets() -> (
logger.warning("collect_ai_catalog_failed", error=str(e))
collector_errors.append("ai_catalog")
# 12. Gitea repository and backup targets from live Prometheus receipts.
try:
gitea_assets, gitea_relationships = await _collect_gitea_bundle_assets()
assets.extend(gitea_assets)
relationships.extend(gitea_relationships)
if gitea_assets:
reconciled_prefixes.update(
("gitea/repo/", "backup/gitea-bundle/")
)
except Exception as e:
logger.warning("collect_gitea_bundle_failed", error=str(e))
collector_errors.append("gitea_bundle_backup")
return (
list({asset["asset_key"]: asset for asset in assets}.values()),
relationships,
@@ -969,6 +983,127 @@ async def _collect_runtime_assets() -> (
)
def _build_gitea_bundle_assets(
payload: dict[str, Any],
) -> tuple[list[dict[str, Any]], list[dict[str, str]]]:
"""Project live bundle metrics into repo and backup asset identities."""
if payload.get("readback_present") is not True:
raise RuntimeError("gitea_bundle_readback_unavailable")
host = str(payload.get("host") or "").strip()
if not re.fullmatch(r"[A-Za-z0-9_.-]+", host):
raise RuntimeError("gitea_bundle_host_invalid")
repo_rows = payload.get("repos") or []
if not isinstance(repo_rows, list) or not repo_rows:
raise RuntimeError("gitea_bundle_repo_rows_missing")
backup_key = f"backup/gitea-bundle/{host}"
summary = payload.get("summary") or {}
assets: list[dict[str, Any]] = [
{
"asset_key": backup_key,
"asset_type": "backup_target",
"host": host,
"namespace": None,
"name": f"gitea-bundle-{host}",
"metadata": {
"kind": "GiteaRepositoryBundleTarget",
"status": str(payload.get("status") or "unknown"),
"ready": payload.get("ready") is True,
"expected_repo_count": int(summary.get("expected_repo_count") or 0),
"repo_row_count": int(summary.get("repo_row_count") or len(repo_rows)),
"missing_repo_count": int(
summary.get("expected_repo_missing_count") or 0
),
"failed_repo_count": int(summary.get("failed_repo_count") or 0),
"checksum_missing_count": int(
summary.get("checksum_missing_count") or 0
),
"bundle_fresh": summary.get("bundle_fresh") is True,
"sample_restore_dry_run_ok": (
summary.get("sample_restore_dry_run_ok") is True
),
"source_schema_version": str(
payload.get("schema_version") or "unknown"
),
"secret_value_read_by_scanner": False,
"repository_content_read_by_scanner": False,
},
"tags": [
"source:gitea_bundle_metrics",
"owner:platform_security",
],
}
]
relationships: list[dict[str, str]] = []
seen_repos: set[str] = set()
for row in repo_rows:
if not isinstance(row, dict):
continue
repo = str(row.get("repo") or "").strip().strip("/")
if not re.fullmatch(
r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+",
repo,
):
continue
if repo in seen_repos:
continue
seen_repos.add(repo)
repo_key = f"gitea/repo/{repo}"
assets.append(
{
"asset_key": repo_key,
"asset_type": "gitea_repo",
"host": host,
"namespace": repo.split("/", 1)[0],
"name": repo,
"metadata": {
"kind": "GiteaRepository",
"bundle_present": row.get("present") is True,
"bundle_verified": row.get("ok") is True,
"head_count": int(row.get("head_count") or 0),
"checksum_digest_present": (
row.get("checksum_digest_present") is True
),
"source_schema_version": str(
payload.get("schema_version") or "unknown"
),
"repository_content_read_by_scanner": False,
"secret_value_read_by_scanner": False,
},
"tags": [
"source:gitea_bundle_metrics",
"owner:platform_security",
],
}
)
relationships.append(
{
"from_key": repo_key,
"to_key": backup_key,
"relationship_type": "backs_up_to",
}
)
if not seen_repos:
raise RuntimeError("gitea_bundle_repo_identities_invalid")
return assets, relationships
async def _collect_gitea_bundle_assets() -> (
tuple[list[dict[str, Any]], list[dict[str, str]]]
):
from src.services.gitea_repo_bundle_backup_readback import (
load_latest_gitea_repo_bundle_backup_readback,
)
payload = await asyncio.to_thread(load_latest_gitea_repo_bundle_backup_readback)
return _build_gitea_bundle_assets(payload)
def _is_valid_ipv4(s: str) -> bool:
"""嚴格 IPv4 判斷: 4 段 + 每段 0-255 整數.