Compare commits

..
Author SHA1 Message Date
Cédric VerstraetenandCopilot App 54b10967b2 Record approved expiring exception for exact shared signing key
Preserve the HIGH finding and auditable approval; fail closed on expiry, file or finding changes, and keep all other security gates enforced.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-11-05 12:37:10 +01:00
Cédric VerstraetenandCopilot App 7b95de7c10 Document user-directed retention of recording signing key
Preserve the unresolved release finding without inventing an exception or changing recording trust.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-11-05 10:28:08 +01:00
Cédric VerstraetenandCopilot App 29537da6e8 Replace obsolete SVG loader with tested maintained SVGR alias
Preserve precompiled shared UI and CRA rendering, explicitly scan the real replacement identity, and guard against unsupported legacy loader calls.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-11-05 08:55:30 +01:00
Cédric VerstraetenandCopilot App 69ac25396d Use consistent API regression test formatting
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-11-05 08:39:42 +01:00
Cédric VerstraetenandCopilot App 04beb83540 Fix scanner findings and rootfs assembly lint failures
Patch Go and compatible UI dependency paths; verify legacy toolchain integrations without bypassing release gates.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-11-05 08:30:19 +01:00
Cédric VerstraetenandCopilot App 85dcb841e2 Require patched Go floor and update network dependencies
Align source security scanning with the verified Go1.26.10 compiler, disallow older vulnerable patch levels and update x/net. Preserve the remaining untriaged crypto advisory as a blocking finding rather than selecting an unreleased fixed version.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-11-04 22:30:14 +01:00
Cédric VerstraetenandCopilot App 322ac7d7c9 Support classic Docker exports in native PR checks
Retain full Buildx metadata provenance without exporting an attested index to the classic Docker store. Strengthen healthcheck assertions for redirect rejection and deadline errors.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-11-04 22:15:25 +01:00
Cédric VerstraetenandCopilot App e6f0c068da Install pinned scanner without release API credentials
Collect native runtime evidence before scanner setup and retain explicit setup failures while keeping security gates fail-closed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-11-04 22:09:22 +01:00
Cédric VerstraetenandCopilot App e3acd64a57 Align scratch runtime and releases with image hardening baseline
Move container HTTP to8080, remove capabilities/curl, preserve demo UI without mutating assets, patch vulnerable Go dependencies and document migration/runtime contracts. Gate releases on source/final-image security evidence and unresolved policy exceptions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-11-04 22:03:04 +01:00
Cédric VerstraetenandCopilot App 9553b1b4b9 Assemble a media-compatible scratch runtime image
Build an explicit architecture-aware runtime filesystem from installed Debian package dependency manifests, preserving dynamic loaders, codecs and plugins, generated library alternatives/caches, trust data, non-root ownership and port binding. Keep shell/PTY and common runtime utilities while omitting package-management and privilege tools. Expand image smoke checks for H264/H265, font rendering, shell/PTY, package provenance, writable caches and existing startup/volume behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-11-04 21:15:54 +01:00
Cédric VerstraetenandCopilot App f6ce5e0846 Pin Go 1.26 toolchain and verified multi-platform image bases
Use Go 1.26.10 locally without automatic toolchain switching, verify downloaded modules, and build with read-only module metadata and trimmed source/VCS paths. Pin Go, Node, and Debian multi-platform manifests while retaining the compatible CGO/FFmpeg runtime and existing architecture selection. Align Go CI and document pin-refresh and ARMv6 limitations.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-11-04 20:53:40 +01:00
Cédric VerstraetenandCopilot App 024bb0abf9 Harden production Docker image and verify runtime permissions
Exclude local secrets and mutable data from image builds, restore Go checksum verification, remove runtime capability tooling and setuid/setgid permissions, and preserve non-root media and demo startup behavior. Add native image smoke checks and document runtime constraints.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-11-04 20:42:21 +01:00
41 changed files with 7529 additions and 4262 deletions
+42
View File
@@ -0,0 +1,42 @@
# Only application sources are needed by the production build.
**
!Dockerfile
!machinery/
!machinery/**
!ui/
!ui/**
!deployments/
deployments/*
!deployments/docker/
deployments/docker/*
!deployments/docker/assemble-rootfs.sh
**/.git
**/.env
**/.env.*
**/.git-credentials
**/.netrc
**/.npmrc
**/.yarnrc.yml
**/.aws
**/.ssh
**/.kube
**/node_modules
**/.DS_Store
**/*.test
**/*.pprof
**/coverage.out
machinery/main
machinery/vendor
machinery/go.work
machinery/go.work.sum
machinery/www
machinery/data/**
!machinery/data/config/
!machinery/data/config/config.json
!machinery/data/config/user.json
!machinery/data/test-480p.mp4
ui/build
ui/coverage
ui/.npmrc
ui/.yarnrc
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
"""Validate BuildKit provenance and retain resolved base-image materials."""
import json
from pathlib import Path
import re
import sys
def materials(value):
if isinstance(value, dict):
if "uri" in value and value["uri"].startswith(("pkg:docker/", "docker-image://")):
digest = value.get("digest", {}).get("sha256", "")
if not re.fullmatch(r"[a-f0-9]{64}", digest):
raise ValueError(f"Missing base-image digest: {value['uri']}")
yield value
for child in value.values():
yield from materials(child)
elif isinstance(value, list):
for child in value:
yield from materials(child)
if __name__ == "__main__":
directory = Path(sys.argv[1])
provenance = json.loads((directory / "provenance.json").read_text())
bases = list(materials(provenance))
if not bases:
raise SystemExit("Missing provenance/base-image materials")
(directory / "base-image-digests.json").write_text(json.dumps(bases, indent=2) + "\n")
+242
View File
@@ -0,0 +1,242 @@
#!/usr/bin/env python3
"""Fail-closed source/application and image scans with redacted evidence."""
import argparse
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import subprocess
import sys
def secret_identity(target, finding):
return (target, finding.get("RuleID"), finding.get("Category"),
finding.get("Severity"), finding.get("StartLine"), finding.get("EndLine"))
def approve_shared_key(report, source_root, policy, now=None):
"""Bind the one reviewed exception to an unchanged file and exact finding."""
now = now or datetime.now(timezone.utc)
records = policy.get("exceptions")
if not isinstance(records, list) or len(records) != 1:
raise ValueError("Exactly one shared-key exception record is required")
record = records[0]
if not isinstance(record, dict) or record.get("id") != "shared-recording-signing-key":
raise ValueError("Unexpected scan exception")
if record.get("status") != "approved" or not all(
isinstance(record.get(key), str) and record[key].strip()
for key in ("owner", "approved_by", "reason", "approval_url", "expires")
):
raise ValueError("Shared-key exception lacks approval/owner/reason/evidence/expiry")
if record["approval_url"] != "https://github.com/kerberos-io/agent/pull/350":
raise ValueError("Shared-key exception must reference its reviewed PR record")
expires = datetime.fromisoformat(record["expires"].replace("Z", "+00:00"))
if expires.tzinfo is None or expires <= now:
raise ValueError("Shared-key exception expired or lacks timezone")
if (record.get("target") != "machinery/src/config/main.go"
or record.get("rule_id") != "private-key"
or record.get("category") != "AsymmetricPrivateKey"
or record.get("severity") != "HIGH"
or type(record.get("start_line")) is not int
or record["start_line"] < 1
or record.get("end_line") != record["start_line"]):
raise ValueError("Shared-key exception has an unexpected finding scope")
root = Path(source_root).resolve()
path = root / record["target"]
if path.resolve() != path or hashlib.sha256(path.read_bytes()).hexdigest() != record.get("file_sha256"):
raise ValueError("Shared-key source file changed or is a symlink; renewed review required")
expected = (record["target"], record["rule_id"], record["category"],
record["severity"], record["start_line"], record["end_line"])
matches = [
finding for result in report["Results"]
for finding in result.get("Secrets", [])
if secret_identity(result.get("Target"), finding) == expected
]
if len(matches) != 1:
raise ValueError("Shared-key exception must match exactly one existing source finding")
return [dict(record)]
def check_report(report, image=False, source=False, approved_exceptions=()):
if report.get("SchemaVersion") != 2 or not isinstance(report.get("Results"), list):
raise ValueError("Missing or unsupported Trivy report")
failures = []
approved_secrets = {
(record["target"], record["rule_id"], record["category"], record["severity"],
record["start_line"], record["end_line"])
for record in approved_exceptions
} if source and not image else set()
for result in report["Results"]:
if result.get("Errors"):
failures.append(f"Scan errors in {result.get('Target')}: {result['Errors']}")
if result.get("MisconfSummary", {}).get("Exceptions", 0):
failures.append(f"Unapproved misconfiguration exceptions in {result.get('Target')}")
for kind in ("Vulnerabilities", "Secrets", "Misconfigurations"):
for finding in result.get(kind, []):
if kind == "Misconfigurations" and finding.get("Status") == "PASS":
continue
if kind == "Misconfigurations" and finding.get("Status") != "FAIL":
failures.append(f"Unexpected/suppressed misconfiguration status: {finding.get('Status')}")
severity = finding.get("Severity", "UNKNOWN")
if kind == "Secrets" and secret_identity(result.get("Target"), finding) in approved_secrets:
continue
if severity not in ("LOW", "MEDIUM"):
failures.append(f"{result.get('Target')}: {kind}: "
f"{finding.get('VulnerabilityID', finding.get('ID', finding.get('RuleID')))} "
f"{severity}")
if image:
# A scratch image without package metadata must not yield a false clean OS scan.
if not report.get("Metadata", {}).get("OS", {}).get("Family"):
failures.append("No OS detected: preserve runtime OS/package metadata")
if not any(r.get("Class") == "os-pkgs" and r.get("Packages")
for r in report["Results"]):
failures.append("No OS package inventory detected")
if not any(r.get("Type") == "gobinary" and r.get("Packages")
for r in report["Results"]):
failures.append("No Go application package inventory detected")
if source and not any(
r.get("Type") == "yarn" and r.get("Target", "").replace("\\", "/").endswith("ui/yarn.lock")
and r.get("Packages") for r in report["Results"]
):
failures.append("Missing production UI yarn.lock package inventory")
return failures
def redact_report(value):
"""Keep findings actionable without persisting matches, excerpts or image env/history."""
if isinstance(value, list):
return [redact_report(item) for item in value]
if not isinstance(value, dict):
return value
result = {}
for key, child in value.items():
if key.lower() in {"imageconfig", "buildinfo"}:
continue
if key.lower() in {
"match", "code", "lines", "source", "sourcecode", "snippet", "context",
"content",
}:
result[key] = "[REDACTED]"
elif key == "Errors":
result[key] = ["[REDACTED scanner error]"] if child else []
elif key in ("Secrets", "Misconfigurations"):
allowed = {
"ID", "AVDID", "RuleID", "Type", "Category", "Title", "Severity",
"Status", "StartLine", "EndLine", "References",
}
result[key] = [
{field: finding[field] for field in allowed if field in finding}
| {"SourceExcerpt": "[REDACTED]"}
for finding in child
]
else:
result[key] = redact_report(child)
return result
def scan(target, output, platform=None):
output.mkdir(parents=True, exist_ok=True)
config = output / "scanner.yaml"
ignore = output / "empty.ignore"
secret_config = output / "secret.yaml"
config.write_text("{}\n")
secret_config.write_text("{}\n")
ignore.write_text("")
# Do not consume repository trivy.yaml/.trivyignore or TRIVY_* overrides.
env = {k: v for k, v in os.environ.items() if not k.startswith("TRIVY_")}
command = [
"trivy", "image" if platform else "fs", "--config", str(config),
"--cache-dir", ".ci-tools/trivy-cache",
"--ignorefile", str(ignore), "--secret-config", str(secret_config),
"--format", "json", "--exit-code", "0",
"--scanners", "vuln,secret,misconfig",
"--severity", "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL", "--timeout", "20m",
"--list-all-pkgs", "--detection-priority", "comprehensive",
]
if platform:
command += ["--platform", platform, "--pkg-types", "os,library",
"--image-config-scanners", "secret,misconfig"]
else:
# Include development/build dependencies too: none are silently exempted.
# Trivy marks Dev packages using the adjacent package.json and yarn.lock.
command += ["--pkg-types", "library", "--include-dev-deps"]
command.append(target)
# Never write raw scanner output, even on scanner failure/interruption. stderr
# may contain source excerpts too; retain only a safe exit-status diagnostic.
completed = subprocess.run(command, env=env, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, text=True, check=False)
(output / "scanner.log").write_text(
f"Trivy exit status: {completed.returncode}\n"
"Raw scanner stdout/stderr omitted; scan.json is redacted before persistence.\n")
if completed.returncode:
raise ValueError(f"Trivy failed with exit status {completed.returncode}; raw diagnostics withheld")
try:
report = redact_report(json.loads(completed.stdout))
except ValueError:
raise ValueError("Invalid scanner JSON; raw output withheld") from None
(output / "scan.json").write_text(json.dumps(report, indent=2) + "\n")
approved = []
exception_failures = []
if not platform:
try:
policy = json.loads(Path(__file__).with_name("image-scan-exceptions.json").read_text())
(output / "exception-policy.json").write_text(json.dumps(policy, indent=2) + "\n")
approved = approve_shared_key(report, target, policy)
except (OSError, ValueError, TypeError, KeyError) as error:
exception_failures.append(f"Shared-key exception invalid: {error}")
(output / "exception-decisions.json").write_text(json.dumps({
"approved_exceptions": approved, "failures": exception_failures,
}, indent=2) + "\n")
failures = exception_failures + check_report(
report, image=bool(platform), source=not platform, approved_exceptions=approved)
if not platform:
inventory = [{
"target": result.get("Target"), "type": result.get("Type"),
"packages": [{"name": pkg.get("Name"), "version": pkg.get("Version"),
"scope": "development" if pkg.get("Dev") else "production-or-unclassified"}
for pkg in result.get("Packages", [])],
} for result in report["Results"] if result.get("Packages")]
(output / "application-inventory.json").write_text(json.dumps({
"policy": "All production and development/build dependencies are gated; "
"only the reviewed, exact shared-key secret exception may apply.",
"inventories": inventory,
}, indent=2) + "\n")
converted = subprocess.run([
"trivy", "convert", "--cache-dir", ".ci-tools/trivy-cache",
"--format", "spdx-json", "--output", str(output / "sbom.spdx.json"),
str(output / "scan.json"),
], env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
if converted.returncode:
raise ValueError(f"SPDX conversion failed with exit status {converted.returncode}")
sbom = json.loads((output / "sbom.spdx.json").read_text())
if not sbom.get("spdxVersion") or not sbom.get("packages"):
failures.append("Missing/empty SPDX SBOM")
return failures
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("target", help="Repository root, or final image reference with --platform")
parser.add_argument("output", type=Path, help="Directory for redacted reports and SPDX SBOM")
parser.add_argument("--platform", help="Image platform, e.g. linux/amd64; omit for source/UI scanning")
args = parser.parse_args()
args.output.mkdir(parents=True, exist_ok=True)
approved = []
try:
failures = scan(args.target, args.output, args.platform)
approved = json.loads((args.output / "exception-decisions.json").read_text())["approved_exceptions"]
except (OSError, ValueError, subprocess.CalledProcessError) as error:
failures = [f"Scanner/evidence error: {error}"]
(args.output / "policy.json").write_text(json.dumps({
"target": args.target, "platform": args.platform, "failures": failures,
"approved_exceptions": approved, "passed": not failures,
}, indent=2) + "\n")
for failure in failures:
print(failure, file=sys.stderr)
return int(bool(failures))
if __name__ == "__main__":
sys.exit(main())
+13
View File
@@ -0,0 +1,13 @@
{
"required_exceptions": [
{
"id": "remote-terminal-pty",
"status": "pending",
"reason": "The product retains functional remote terminal/PTY support, contrary to the no-shell baseline.",
"owner": null,
"expires": null,
"approved_by": null,
"approval_url": null
}
]
}
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Release-only policy gate. Approval records require a reviewed repository change."""
from datetime import datetime, timezone
import json
from pathlib import Path
import sys
def check_policy(policy, now=None):
now = now or datetime.now(timezone.utc)
records = policy.get("required_exceptions")
if not isinstance(records, list) or not records:
return ["Required exception records are missing"]
if {record.get("id") for record in records} != {"remote-terminal-pty"}:
return ["Required remote-terminal-pty exception record is missing or unexpected"]
failures = []
for record in records:
name = record["id"]
if record.get("status") != "approved":
failures.append(f"{name}: required exception is PENDING, not approved")
continue
if not all(isinstance(record.get(key), str) and record[key].strip()
for key in ("owner", "approved_by", "reason", "approval_url", "expires")):
failures.append(f"{name}: missing owner/approver/reason/approval evidence/expiry")
continue
if not record["approval_url"].startswith("https://github.com/kerberos-io/agent/"):
failures.append(f"{name}: approval must reference this repository's reviewed record")
try:
expires = datetime.fromisoformat(record["expires"].replace("Z", "+00:00"))
if expires.tzinfo is None or expires <= now:
failures.append(f"{name}: approval expired or lacks timezone")
except ValueError:
failures.append(f"{name}: invalid approval expiry")
return failures
if __name__ == "__main__":
try:
failures = check_policy(json.loads(Path(sys.argv[1]).read_text()))
except (OSError, ValueError, TypeError, KeyError) as error:
failures = [f"Invalid release policy: {error}"]
for failure in failures:
print(failure, file=sys.stderr)
sys.exit(bool(failures))
@@ -0,0 +1,20 @@
{
"exceptions": [
{
"id": "shared-recording-signing-key",
"status": "approved",
"owner": "@cedricve",
"approved_by": "@cedricve",
"approval_url": "https://github.com/kerberos-io/agent/pull/350",
"expires": "2026-12-05T23:59:59Z",
"reason": "User approved temporary retention of the existing shared recording-signing key to preserve recording verification trust and startup behavior. Anyone possessing this shared private key can forge signatures under that identity.",
"target": "machinery/src/config/main.go",
"file_sha256": "e28a0bf6ae892559709e8ec534cfc7b7cb55180e33415461d966c1afe53ca25b",
"rule_id": "private-key",
"category": "AsymmetricPrivateKey",
"severity": "HIGH",
"start_line": 710,
"end_line": 710
}
]
}
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
# Public OCI index pins the platform-specific executable without GitHub API access.
version=0.76.1
image=ghcr.io/aquasecurity/trivy:0.76.1@sha256:790fd66d1697395d8693d1acaa1ea702406a20f109b6fc7bac8db623eca6c06c
evidence_dir="${1:-evidence/scanner-install}"
mkdir -p "$evidence_dir" .ci-tools
stage=host-platform
platform=
cid=
finish() {
result=$?
trap - EXIT
if [[ -n "$cid" ]]; then
docker rm -f "$cid" >/dev/null 2>&1 || true
fi
rm -f .ci-tools/trivy.new
python3 - "$evidence_dir/install.json" "$result" "$stage" "$image" "$platform" "$version" <<'PY'
import hashlib
import json
from pathlib import Path
import sys
path, code, stage, image, platform, version = sys.argv[1:]
record = {
"status": "installed" if code == "0" else "failed",
"exit_code": int(code), "stage": stage, "image": image, "platform": platform,
"binary_source": "/usr/local/bin/trivy", "expected_version": version,
}
if code == "0":
record["binary_sha256"] = hashlib.sha256(Path(".ci-tools/trivy").read_bytes()).hexdigest()
Path(path).write_text(json.dumps(record, indent=2) + "\n")
PY
exit "$result"
}
trap finish EXIT
case "$(uname -m)" in
x86_64) platform=linux/amd64 ;;
aarch64|arm64) platform=linux/arm64 ;;
*) echo "Unsupported scanner host architecture" >&2; exit 1 ;;
esac
stage=pull
docker pull --platform "$platform" "$image"
stage=create
cid=$(docker create --platform "$platform" "$image")
stage=extract
docker cp "$cid:/usr/local/bin/trivy" .ci-tools/trivy.new
chmod 0755 .ci-tools/trivy.new
stage=verify-version
.ci-tools/trivy.new --version | grep -Fx "Version: $version"
mv .ci-tools/trivy.new .ci-tools/trivy
stage=publish-path
if [[ -n "${GITHUB_PATH:-}" ]]; then
echo "$PWD/.ci-tools" >> "$GITHUB_PATH"
fi
stage=complete
+288
View File
@@ -0,0 +1,288 @@
#!/usr/bin/env python3
"""Focused release guard tests; no registry, signing, or scanner network access."""
from datetime import datetime, timezone
import copy
import hashlib
import importlib.util
import json
from pathlib import Path
import unittest
from unittest.mock import patch
import subprocess
import tempfile
ROOT = Path(__file__).parent
def load(name):
spec = importlib.util.spec_from_file_location(name, ROOT / f"{name}.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
scanner = load("image-policy")
release = load("image-release-policy")
evidence = load("image-evidence")
class ScanPolicyTests(unittest.TestCase):
def report(self, severity=None, kind="Vulnerabilities"):
result = {"Class": "os-pkgs", "Packages": [{"Name": "libc6"}]}
if severity:
result[kind] = [{"Severity": severity, "Status": "FAIL", "ID": "fixture"}]
return {"SchemaVersion": 2, "Metadata": {"OS": {"Family": "debian"}},
"Results": [result, {"Class": "lang-pkgs", "Type": "gobinary",
"Packages": [{"Name": "stdlib"}]}]}
def test_low_and_medium_are_not_blocking(self):
for severity in ("LOW", "MEDIUM"):
self.assertEqual(scanner.check_report(self.report(severity), image=True), [])
def test_high_critical_unknown_and_unrecognized_block(self):
for kind in ("Vulnerabilities", "Secrets", "Misconfigurations"):
for severity in ("HIGH", "CRITICAL", "UNKNOWN", "invalid"):
with self.subTest(kind=kind, severity=severity):
self.assertTrue(scanner.check_report(self.report(severity, kind), image=True))
def test_missing_inventory_blocks_false_clean_scratch_scan(self):
report = self.report()
report["Metadata"] = {}
report["Results"] = []
self.assertEqual(len(scanner.check_report(report, image=True)), 3)
def test_empty_os_inventory_blocks(self):
report = self.report()
report["Results"][0]["Packages"] = []
self.assertTrue(scanner.check_report(report, image=True))
def test_report_errors_and_suppression_summary_block(self):
report = self.report()
report["Results"][0]["Errors"] = ["fixture scanner error"]
report["Results"][0]["MisconfSummary"] = {"Exceptions": 1}
self.assertEqual(len(scanner.check_report(report)), 2)
def test_missing_or_unsupported_report_blocks(self):
for report in ({}, {"SchemaVersion": 1, "Results": []}):
with self.assertRaises(ValueError):
scanner.check_report(report)
def test_suppressed_misconfiguration_blocks(self):
report = self.report("LOW", "Misconfigurations")
report["Results"][0]["Misconfigurations"][0]["Status"] = "EXCEPTION"
self.assertTrue(scanner.check_report(report))
def test_scanner_errors_are_not_ignored(self):
directory = unittest.mock.MagicMock()
with patch.object(scanner.subprocess, "run",
side_effect=subprocess.CalledProcessError(1, ["trivy"])):
with self.assertRaises(subprocess.CalledProcessError):
scanner.scan("fixture", directory, "linux/amd64")
def test_source_requires_ui_lockfile_inventory(self):
report = self.report()
self.assertTrue(scanner.check_report(report, source=True))
report["Results"].append({"Type": "yarn", "Target": "ui/yarn.lock",
"Packages": [{"Name": "react", "Version": "17.0.2"}]})
self.assertEqual(scanner.check_report(report, source=True), [])
report["Results"][-1]["Vulnerabilities"] = [{"Severity": "HIGH", "VulnerabilityID": "fixture"}]
self.assertTrue(scanner.check_report(report, source=True))
def test_secret_and_source_excerpts_are_removed_without_waiving_findings(self):
marker = "sensitive-fixture-text-not-a-real-credential"
report = self.report("HIGH", "Secrets")
report["Metadata"]["ImageConfig"] = {"config": {"Env": [marker]}}
report["Metadata"]["BuildInfo"] = {"attrs": {"secret": marker}}
report["Results"][0]["Secrets"][0].update(
Match=marker, Code={"Lines": [{"Content": marker}]}, Message=marker,
UnexpectedFutureExcerptField=marker, RuleID="private-key-fixture")
report["Results"][0]["Misconfigurations"] = [{
"ID": "fixture", "Severity": "HIGH", "Status": "FAIL",
"Message": marker, "CauseMetadata": {"Code": {"Lines": [{"Content": marker}]}}
}]
report["Results"][0]["Errors"] = [marker]
redacted = scanner.redact_report(report)
self.assertNotIn(marker, json.dumps(redacted))
self.assertNotIn("ImageConfig", redacted["Metadata"])
self.assertTrue(scanner.check_report(redacted, image=True))
self.assertEqual(redacted["Results"][0]["Secrets"][0]["RuleID"], "private-key-fixture")
def test_source_scan_captures_output_in_memory_and_generates_sbom(self):
report = self.report()
report["Results"].append({"Type": "yarn", "Target": "ui/yarn.lock",
"Packages": [{"Name": "fixture", "Version": "1.0", "Dev": True}]})
directory = unittest.mock.MagicMock()
directory.__truediv__.return_value.read_text.return_value = json.dumps({
"spdxVersion": "SPDX-2.3", "packages": [{"name": "fixture"}]})
completed = subprocess.CompletedProcess(["trivy"], 0, stdout=json.dumps(report), stderr="")
with patch.object(scanner.subprocess, "run", return_value=completed) as run, \
patch.object(scanner, "approve_shared_key", return_value=[]):
self.assertEqual(scanner.scan("fixture", directory), [])
command = run.call_args_list[0].args[0]
self.assertIn("--include-dev-deps", command)
self.assertIn("vuln,secret,misconfig", command)
self.assertNotIn("--output", command)
self.assertEqual(run.call_args_list[0].kwargs["stdout"], subprocess.PIPE)
self.assertEqual(run.call_args_list[1].args[0][1], "convert")
def test_raw_scanner_error_diagnostics_are_not_persisted(self):
directory = unittest.mock.MagicMock()
marker = "sensitive-fixture-text-not-a-real-credential"
completed = subprocess.CompletedProcess(["trivy"], 1, stdout=marker, stderr=marker)
with patch.object(scanner.subprocess, "run", return_value=completed):
with self.assertRaisesRegex(ValueError, "exit status 1"):
scanner.scan("fixture", directory)
self.assertNotIn(marker, str(directory.mock_calls))
class SharedKeyExceptionTests(unittest.TestCase):
NOW = datetime(2026, 11, 5, tzinfo=timezone.utc)
def setUp(self):
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.root = Path(self.directory.name)
self.path = self.root / "machinery/src/config/main.go"
self.path.parent.mkdir(parents=True)
self.path.write_text("non-secret source fixture\n")
self.policy = json.loads((ROOT / "image-scan-exceptions.json").read_text())
self.record = self.policy["exceptions"][0]
self.record["file_sha256"] = hashlib.sha256(self.path.read_bytes()).hexdigest()
self.finding = {
"RuleID": self.record["rule_id"], "Category": self.record["category"],
"Severity": "HIGH", "StartLine": 710, "EndLine": 710,
}
self.report = {"SchemaVersion": 2, "Results": [
{"Target": self.record["target"], "Secrets": [self.finding]},
{"Target": "ui/yarn.lock", "Type": "yarn", "Packages": [{"Name": "react"}]},
]}
def approved(self):
return scanner.approve_shared_key(self.report, self.root, self.policy, self.NOW)
def test_exact_key_is_approved_but_finding_is_preserved(self):
original = copy.deepcopy(self.report)
approved = self.approved()
self.assertTrue(scanner.check_report(self.report, source=True))
self.assertEqual(scanner.check_report(
self.report, source=True, approved_exceptions=approved), [])
self.assertEqual(self.report, original)
self.assertEqual(approved[0]["id"], "shared-recording-signing-key")
def test_other_secrets_and_vulnerabilities_still_block(self):
approved = self.approved()
result = self.report["Results"][0]
result["Secrets"].append(dict(self.finding, StartLine=711, EndLine=711))
result["Vulnerabilities"] = [{"VulnerabilityID": "fixture", "Severity": "CRITICAL"}]
self.assertEqual(len(scanner.check_report(
self.report, source=True, approved_exceptions=approved)), 2)
def test_exception_never_applies_to_image_or_non_source_scans(self):
approved = self.approved()
self.assertTrue(scanner.check_report(self.report, approved_exceptions=approved))
self.assertTrue(scanner.check_report(
self.report, image=True, source=True, approved_exceptions=approved))
def test_expiry_is_enforced_at_boundary(self):
expiry = datetime(2026, 12, 5, 23, 59, 59, tzinfo=timezone.utc)
with self.assertRaisesRegex(ValueError, "expired"):
scanner.approve_shared_key(self.report, self.root, self.policy, expiry)
self.record["expires"] = "2026-12-05"
with self.assertRaisesRegex(ValueError, "timezone"):
self.approved()
def test_incomplete_or_unapproved_metadata_blocks(self):
for field in ("owner", "approved_by", "reason", "approval_url", "expires", "status"):
with self.subTest(field=field):
original = self.record[field]
self.record[field] = ""
with self.assertRaises(ValueError):
self.approved()
self.record[field] = original
self.record["approval_url"] = "https://example.com/unreviewed"
with self.assertRaises(ValueError):
self.approved()
def test_changed_source_or_symlink_requires_review(self):
self.path.write_text("changed source fixture\n")
with self.assertRaisesRegex(ValueError, "changed"):
self.approved()
target = self.root / "other.go"
self.path.rename(target)
self.path.symlink_to(target)
with self.assertRaisesRegex(ValueError, "symlink"):
self.approved()
def test_changed_finding_does_not_match(self):
for field, value in (("RuleID", "other"), ("Category", "other"),
("Severity", "CRITICAL"), ("StartLine", 711), ("EndLine", 711)):
with self.subTest(field=field):
original = self.finding[field]
self.finding[field] = value
with self.assertRaisesRegex(ValueError, "exactly one"):
self.approved()
self.finding[field] = original
def test_duplicate_missing_or_broadened_records_block(self):
self.policy["exceptions"].append(dict(self.record))
with self.assertRaises(ValueError):
self.approved()
self.policy["exceptions"].pop()
self.report["Results"][0]["Secrets"].append(dict(self.finding))
with self.assertRaisesRegex(ValueError, "exactly one"):
self.approved()
self.report["Results"][0]["Secrets"] = []
with self.assertRaisesRegex(ValueError, "exactly one"):
self.approved()
self.record["target"] = "../other.go"
with self.assertRaisesRegex(ValueError, "scope"):
self.approved()
def test_checked_in_record_matches_unchanged_source(self):
report = copy.deepcopy(self.report)
policy = json.loads((ROOT / "image-scan-exceptions.json").read_text())
self.assertEqual(len(scanner.approve_shared_key(
report, ROOT.parent.parent, policy, self.NOW)), 1)
class ReleasePolicyTests(unittest.TestCase):
NOW = datetime(2026, 11, 4, tzinfo=timezone.utc)
def test_checked_in_state_blocks_publication(self):
state = json.loads((ROOT / "image-release-policy.json").read_text())
self.assertTrue(release.check_policy(state, self.NOW))
def test_missing_records_block(self):
for state in ({}, {"required_exceptions": []},
{"required_exceptions": [{"id": "unrelated"}]}):
self.assertTrue(release.check_policy(state, self.NOW))
def test_expired_or_incomplete_approval_blocks(self):
state = json.loads((ROOT / "image-release-policy.json").read_text())
record = state["required_exceptions"][0]
record["status"] = "approved"
self.assertTrue(release.check_policy(state, self.NOW))
record.update(owner="test-fixture", approved_by="test-fixture",
approval_url="https://github.com/kerberos-io/agent/issues/1",
expires="2020-01-01T00:00:00Z")
self.assertTrue(release.check_policy(state, self.NOW))
record["expires"] = "invalid"
self.assertTrue(release.check_policy(state, self.NOW))
record["expires"] = "2027-01-01"
self.assertTrue(release.check_policy(state, self.NOW))
class ProvenanceTests(unittest.TestCase):
def test_base_digest_retained(self):
material = {"uri": "pkg:docker/debian@trixie", "digest": {"sha256": "a" * 64}}
self.assertEqual(list(evidence.materials({"SLSA": {"materials": [material]}})), [material])
def test_missing_digest_rejected(self):
with self.assertRaises(ValueError):
list(evidence.materials({"uri": "pkg:docker/debian@trixie"}))
if __name__ == "__main__":
unittest.main()
+3 -3
View File
@@ -17,7 +17,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25.x"
go-version: "1.26.10"
check-latest: true
cache: true
cache-dependency-path: machinery/go.sum
@@ -32,9 +32,9 @@ jobs:
libswscale-dev \
pkg-config
- name: Get dependencies
run: cd machinery && go mod download
run: cd machinery && go mod download && go mod verify
- name: Build
run: cd machinery && go build -v ./...
run: cd machinery && go build -mod=readonly -trimpath -buildvcs=false -v ./...
- name: Vet
run: cd machinery && go vet -v ./...
- name: Test
+324
View File
@@ -0,0 +1,324 @@
name: Verified image publication
on:
workflow_call:
inputs:
tag:
type: string
default: ""
source_ref:
type: string
required: true
nightly:
type: boolean
default: false
publish_latest:
type: boolean
default: false
secrets:
DOCKER_USERNAME:
required: true
DOCKER_PASSWORD:
required: true
permissions:
contents: read
# No parallel run can overwrite a customer tag while another run verifies it.
concurrency:
group: image-publication-${{ inputs.nightly && 'nightly' || 'release' }}
cancel-in-progress: false
defaults:
run:
shell: bash
jobs:
resolve:
runs-on: ubuntu-24.04
permissions:
contents: write
outputs:
sha: ${{ steps.source.outputs.sha }}
tag: ${{ steps.source.outputs.tag }}
created: ${{ steps.source.outputs.created }}
matrix: ${{ steps.source.outputs.matrix }}
repository: ${{ steps.source.outputs.repository }}
staging_repository: ${{ steps.source.outputs.staging_repository }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- name: Validate requested tag
env:
TAG: ${{ inputs.tag }}
NIGHTLY: ${{ inputs.nightly }}
run: |
if [[ "$NIGHTLY" != true && ! "$TAG" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_.-]{0,127}$ ]]; then
echo "Invalid Docker tag" >&2
exit 1
fi
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ inputs.source_ref }}
path: source
persist-credentials: false
- name: Resolve once for every architecture
id: source
env:
TAG: ${{ inputs.tag }}
NIGHTLY: ${{ inputs.nightly }}
run: |
sha=$(git -C source rev-parse HEAD)
[[ "$sha" =~ ^[a-f0-9]{40}$ ]]
created=$(date -u +%Y-%m-%dT%H:%M:%SZ)
repository=kerberos/agent
staging_repository=kerberos/agent-arch
matrix='{"include":[{"architecture":"amd64","runner":"ubuntu-24.04"},{"architecture":"arm64","runner":"ubuntu-24.04-arm"}]}'
if [[ "$NIGHTLY" == true ]]; then
TAG="${sha:0:7}"
repository=kerberos/agent-nightly
staging_repository="$repository"
# Keep arm/v6: the pinned Go trixie base currently lacks this platform.
# Its build must fail rather than publishing an incomplete nightly index.
matrix='{"include":[{"architecture":"amd64","runner":"ubuntu-24.04"},{"architecture":"arm64","runner":"ubuntu-24.04-arm"},{"architecture":"arm/v7","runner":"ubuntu-24.04"},{"architecture":"arm/v6","runner":"ubuntu-24.04"}]}'
fi
{
echo "sha=$sha"
echo "tag=$TAG"
echo "created=$created"
echo "repository=$repository"
echo "staging_repository=$staging_repository"
echo "matrix=$matrix"
} >> "$GITHUB_OUTPUT"
mkdir -p evidence
printf '%s\n' "$sha" > evidence/source-revision.txt
printf '%s\n' "$created" > evidence/created.txt
printf '%s\n' "$TAG" > evidence/version.txt
cp .github/scripts/image-release-policy.json evidence/runtime-policy.json
- name: Validate fail-closed policy helpers
run: python3 .github/scripts/test-image-policy.py
- name: Keep existing GitHub release unpublished until gates pass
if: ${{ !inputs.nightly }}
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.source.outputs.tag }}
run: |
# A release:created event may refer to an already public release.
# Return it to draft immediately; do not imply this run has verified it.
gh api --paginate "repos/$GITHUB_REPOSITORY/releases" --jq '.[]' | jq -s . > evidence/releases.json
release_id=$(jq -r --arg tag "$TAG" '.[] | select(.tag_name == $tag) | .id' evidence/releases.json)
if [[ -n "$release_id" ]]; then
gh api --method PATCH "repos/$GITHUB_REPOSITORY/releases/$release_id" -F draft=true > evidence/draft-release.json
fi
- name: Install pinned public OCI scanner
run: bash .github/scripts/install-trivy.sh
- name: Source/UI dependency vulnerability, secret and misconfiguration gate with SPDX SBOM
run: python3 .github/scripts/image-policy.py source evidence/source
- name: Retain source policy evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: image-source-evidence
path: evidence/
retention-days: 90
if-no-files-found: error
build:
needs: resolve
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.resolve.outputs.matrix) }}
env:
ARCH: ${{ matrix.architecture }}
VERSION: ${{ needs.resolve.outputs.tag }}
REVISION: ${{ needs.resolve.outputs.sha }}
CREATED: ${{ needs.resolve.outputs.created }}
STAGING_REPOSITORY: ${{ needs.resolve.outputs.staging_repository }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ needs.resolve.outputs.sha }}
path: source
persist-credentials: false
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
if: ${{ startsWith(matrix.architecture, 'arm/') }}
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build immutable staging image with maximal provenance
id: build
run: |
mkdir -p evidence
arch="${ARCH//\//-}"
echo "arch=$arch" >> "$GITHUB_OUTPUT"
staging="$STAGING_REPOSITORY:ci-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT-$arch"
printf '%s\n' "$REVISION" > evidence/source-revision.txt
docker buildx build --platform "linux/$ARCH" --provenance=mode=max \
--build-arg "VERSION=$VERSION" --build-arg "REVISION=$REVISION" --build-arg "CREATED=$CREATED" \
--label "org.opencontainers.image.version=$VERSION" \
--label "org.opencontainers.image.revision=$REVISION" \
--label "org.opencontainers.image.created=$CREATED" \
--label "org.opencontainers.image.source=https://github.com/$GITHUB_REPOSITORY" \
--metadata-file evidence/build-metadata.json --tag "$staging" --push source
digest=$(jq -er '."containerimage.digest"' evidence/build-metadata.json)
[[ "$digest" =~ ^sha256:[a-f0-9]{64}$ ]]
image="$STAGING_REPOSITORY@$digest"
printf '%s\n' "$image" > evidence/image.txt
docker buildx imagetools inspect "$image" --format '{{json .Provenance}}' > evidence/provenance.json
python3 .github/scripts/image-evidence.py evidence
- name: Extract release payload and smoke test the exact staged image
run: |
image=$(cat evidence/image.txt)
docker pull --platform "linux/$ARCH" "$image"
cid=$(docker create --platform "linux/$ARCH" "$image")
trap 'docker rm -f "$cid" >/dev/null' EXIT
docker cp "$cid:/home/agent" output
# Run the existing smoke interface, including emulated nightly architectures.
DOCKER_DEFAULT_PLATFORM="linux/$ARCH" sh source/deployments/docker/test-image.sh "$image"
arch="${ARCH//\//-}"
tar -cf "agent-$arch.tar" -C output .
rm -rf output
- name: Retain build evidence even on failure
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: image-build-${{ steps.build.outputs.arch || strategy.job-index }}
path: evidence/
retention-days: 90
if-no-files-found: error
- name: Retain existing binary artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: agent-${{ steps.build.outputs.arch }}.tar
path: agent-*.tar
if-no-files-found: error
verify-publish:
needs: [resolve, build]
runs-on: ubuntu-24.04
permissions:
contents: write
id-token: write
env:
REPOSITORY: ${{ needs.resolve.outputs.repository }}
STAGING_REPOSITORY: ${{ needs.resolve.outputs.staging_repository }}
VERSION: ${{ needs.resolve.outputs.tag }}
REVISION: ${{ needs.resolve.outputs.sha }}
PUBLISH_LATEST: ${{ inputs.publish_latest }}
MATRIX: ${{ needs.resolve.outputs.matrix }}
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
path: artifacts
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Install scanner
run: bash .github/scripts/install-trivy.sh
- uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
with:
cosign-release: v3.1.4
- name: Assemble candidate index without publishing version/latest
run: |
mkdir -p evidence
images=()
while IFS= read -r arch; do
directory="artifacts/image-build-${arch//\//-}"
[[ "$(cat "$directory/source-revision.txt")" == "$REVISION" ]]
image=$(cat "$directory/image.txt")
[[ "$image" =~ ^${STAGING_REPOSITORY}@sha256:[a-f0-9]{64}$ ]]
images+=("$image")
done < <(jq -r '.include[].architecture' <<< "$MATRIX")
candidate="$REPOSITORY:ci-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
docker buildx imagetools create --tag "$candidate" "${images[@]}"
digest=$(docker buildx imagetools inspect "$candidate" --format '{{.Manifest.Digest}}')
[[ "$digest" =~ ^sha256:[a-f0-9]{64}$ ]]
image="$REPOSITORY@$digest"
printf '%s\n' "$image" > evidence/final-image.txt
printf '%s\n' "$REVISION" > evidence/source-revision.txt
docker buildx imagetools inspect "$image" --raw > evidence/index.json
docker buildx imagetools inspect "$image" --format '{{json .Provenance}}' > evidence/provenance.json
python3 .github/scripts/image-evidence.py evidence
# Every requested runnable platform must be present, with no silent omissions.
jq -r '.manifests[] | select(.platform.os == "linux") |
.platform | .architecture + (if .variant and .architecture == "arm" then "/" + .variant else "" end)' \
evidence/index.json | sort > evidence/actual-platforms.txt
jq -r '.include[].architecture' <<< "$MATRIX" | sort > evidence/expected-platforms.txt
diff -u evidence/expected-platforms.txt evidence/actual-platforms.txt
- name: Scan every final-index platform and enforce findings/errors
run: |
image=$(cat evidence/final-image.txt)
failed=0
while IFS= read -r arch; do
# Generation uses exit-code 0 only for findings; the helper explicitly
# blocks HIGH/CRITICAL/UNKNOWN and any scanner/inventory/SBOM error.
if ! python3 .github/scripts/image-policy.py "$image" "evidence/${arch//\//-}" --platform "linux/$arch"; then
failed=1
fi
done < <(jq -r '.include[].architecture' <<< "$MATRIX")
exit "$failed"
- name: Required release policy exception gate
run: |
# The product deliberately retains remote terminal/PTY support.
# No approval exists. Keep this release-only gate separate from scans;
# there is no dispatch input/environment variable that can waive it.
cp .github/scripts/image-release-policy.json evidence/runtime-policy.json
python3 .github/scripts/image-release-policy.py evidence/runtime-policy.json
- name: Sign and verify the FINAL multi-platform digest using GitHub OIDC
run: |
image=$(cat evidence/final-image.txt)
# Fulcio's SAN is job_workflow_ref for this local reusable workflow,
# not the caller's filename. Bind the exact repository and executing ref.
# For example: https://github.com/kerberos-io/agent/.github/workflows/image-release.yml@refs/heads/master
identity="https://github.com/$GITHUB_REPOSITORY/.github/workflows/image-release.yml@$GITHUB_REF"
printf '%s\n' "$identity" > evidence/signing-identity.txt
cosign sign --yes --bundle evidence/signature.sigstore.json "$image"
cosign verify --certificate-identity "$identity" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
"$image" > evidence/signature-verification.json
- name: Publish only the already verified and signed digest
run: |
image=$(cat evidence/final-image.txt)
while IFS= read -r arch; do
arch="${arch//\//-}"
staged=$(cat "artifacts/image-build-$arch/image.txt")
# Preserve the existing architecture tag names, after all gates.
docker buildx imagetools create --tag "$STAGING_REPOSITORY:arch-$arch-$VERSION" "$staged"
done < <(jq -r '.include[].architecture' <<< "$MATRIX")
docker buildx imagetools create --tag "$REPOSITORY:$VERSION" "$image"
[[ "$(docker buildx imagetools inspect "$REPOSITORY:$VERSION" --format '{{.Manifest.Digest}}')" == "${image##*@}" ]]
if [[ "$PUBLISH_LATEST" == true ]]; then
docker buildx imagetools create --tag "$REPOSITORY:latest" "$image"
[[ "$(docker buildx imagetools inspect "$REPOSITORY:latest" --format '{{.Manifest.Digest}}')" == "${image##*@}" ]]
fi
- name: Retain final reports/SBOM/provenance/signature even on failure
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: image-final-evidence
path: evidence/
retention-days: 90
if-no-files-found: error
- name: Attach binaries and all evidence, then publish GitHub release
if: ${{ !inputs.nightly }}
run: |
tar -czf image-security-evidence.tar.gz evidence artifacts/image-*
# Create a draft only if absent. API errors must not masquerade as absence.
gh api --paginate "repos/$GITHUB_REPOSITORY/releases" --jq '.[]' | jq -s . > releases.json
if ! jq -e --arg tag "$VERSION" '.[] | select(.tag_name == $tag)' releases.json >/dev/null; then
gh release create "$VERSION" --target "$REVISION" --title "$VERSION" --notes "" --draft
fi
gh release upload "$VERSION" artifacts/agent-*.tar/agent-*.tar image-security-evidence.tar.gz --clobber
gh release edit "$VERSION" --draft=false --latest --title "$VERSION"
+12 -55
View File
@@ -1,62 +1,19 @@
name: Nightly build
on:
# Triggers the workflow every day at 9PM (CET).
schedule:
- cron: "0 22 * * *"
# Allows manual triggering from the Actions tab.
workflow_dispatch:
permissions:
contents: write
jobs:
nightly-build-amd64:
runs-on: ubuntu-latest
strategy:
matrix:
architecture: [amd64]
steps:
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Checkout
uses: actions/checkout@v4
with:
ref: master
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
run: docker buildx build --build-arg VERSION=$(echo $GITHUB_SHA | cut -c1-7) --platform linux/${{matrix.architecture}} -t kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
- name: Create new and append to manifest
run: docker buildx imagetools create -t kerberos/agent-nightly:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
nightly-build-other:
runs-on: ubuntu-latest
strategy:
matrix:
architecture: [arm64, arm/v7, arm/v6]
steps:
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Checkout
uses: actions/checkout@v4
with:
ref: master
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
run: docker buildx build --build-arg VERSION=$(echo $GITHUB_SHA | cut -c1-7) --platform linux/${{matrix.architecture}} -t kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
- name: Create new and append to manifest
run: docker buildx imagetools create --append -t kerberos/agent-nightly:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
nightly:
permissions:
contents: read
id-token: write
uses: ./.github/workflows/image-release.yml
with:
source_ref: master
nightly: true
secrets: inherit
+102 -19
View File
@@ -1,36 +1,119 @@
name: Build pull request
on:
pull_request:
types: [opened, synchronize]
types: [opened, synchronize, reopened]
env:
REPO: kerberos/agent
permissions:
contents: read
defaults:
run:
shell: bash
jobs:
build:
runs-on: ${{ matrix.runner }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- architecture: amd64
runner: ubuntu-24.04
- architecture: arm64
runner: ubuntu-24.04-arm
env:
ARCH: ${{ matrix.architecture }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run Build
run: |
docker build --build-arg VERSION=$(echo "${{ github.event.pull_request.head.sha }}" | cut -c1-7) -t ${{ matrix.architecture }} .
CID=$(docker create ${{matrix.architecture}})
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
docker rm ${CID}
- name: Strip binary
run: tar -cf agent-${{matrix.architecture}}.tar -C output-${{matrix.architecture}} . && rm -rf output-${{matrix.architecture}}
- name: Upload artifact
uses: actions/upload-artifact@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
name: agent-${{matrix.architecture}}.tar
path: agent-${{matrix.architecture}}.tar
persist-credentials: false
- name: Initialize retained runtime/scanner state
run: |
mkdir -p evidence/scanner-install
printf '%s\n' '{"status":"not_started","reason":"waiting_for_native_build_and_smoke"}' \
> evidence/scanner-install/install.json
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ github.sha }}
path: source
persist-credentials: false
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Validate fail-closed policy helpers
run: python3 .github/scripts/test-image-policy.py
- name: Build once, populating OCI metadata from CI
env:
BUILDX_METADATA_PROVENANCE: max
run: |
mkdir -p evidence/build
revision=$(git -C source rev-parse HEAD)
version="${revision:0:7}"
created=$(date -u +%Y-%m-%dT%H:%M:%SZ)
printf '%s\n' "$revision" > evidence/build/source-revision.txt
# Classic Docker cannot load an attested index. Disable attestations
# only for this local export; BUILDX_METADATA_PROVENANCE=max retains
# full build provenance/materials separately, verified below.
docker buildx build --platform "linux/$ARCH" --provenance=false \
--build-arg "VERSION=$version" --build-arg "REVISION=$revision" --build-arg "CREATED=$created" \
--label "org.opencontainers.image.version=$version" \
--label "org.opencontainers.image.revision=$revision" \
--label "org.opencontainers.image.created=$created" \
--label "org.opencontainers.image.source=https://github.com/$GITHUB_REPOSITORY" \
--metadata-file evidence/build/build-metadata.json --tag "$ARCH" \
--output type=docker source
jq -e '."buildx.build.provenance" |
select(type == "object" and length > 0) |
select(.buildConfig | type == "object" and length > 0)' \
evidence/build/build-metadata.json > evidence/build/provenance.json
python3 .github/scripts/image-evidence.py evidence/build
- name: Extract release payload
run: |
cid=$(docker create "$ARCH")
trap 'docker rm -f "$cid" >/dev/null' EXIT
docker cp "$cid:/home/agent" "output-$ARCH"
- name: Test production image
id: smoke
run: sh source/deployments/docker/test-image.sh "$ARCH"
- name: Strip binary
run: tar -cf "agent-$ARCH.tar" -C "output-$ARCH" . && rm -rf "output-$ARCH"
- name: Install pinned public OCI scanner after runtime tests
run: bash .github/scripts/install-trivy.sh
- name: Source/UI dependency and final image gates with SPDX SBOMs
run: |
failed=0
if ! python3 .github/scripts/image-policy.py source evidence/source; then
failed=1
fi
if ! python3 .github/scripts/image-policy.py "$ARCH" "evidence/$ARCH" --platform "linux/$ARCH"; then
failed=1
fi
exit "$failed"
- name: Upload artifact only after secret and vulnerability gates pass
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: agent-${{ matrix.architecture }}.tar
path: agent-${{ matrix.architecture }}.tar
if-no-files-found: error
- name: Record runtime result independently of scanner availability
if: always()
env:
SMOKE_OUTCOME: ${{ steps.smoke.outcome }}
run: |
mkdir -p evidence
python3 - <<'PY'
import json
import os
from pathlib import Path
Path("evidence/runtime-smoke.json").write_text(json.dumps({
"architecture": os.environ["ARCH"],
"outcome": os.environ["SMOKE_OUTCOME"],
"workflow_revision": os.environ["GITHUB_SHA"],
}, indent=2) + "\n")
PY
- name: Retain reports on failure
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: image-pr-evidence-${{ matrix.architecture }}
path: evidence/
retention-days: 90
if-no-files-found: error
+17 -141
View File
@@ -1,160 +1,36 @@
name: Bump release
on:
workflow_dispatch:
inputs:
bump:
description: "Which part of the version to bump"
description: Which part of the version to bump
required: true
default: patch
type: choice
options:
- major
- minor
- patch
options: [major, minor, patch]
permissions:
contents: write
env:
REPO: kerberos/agent
contents: read
jobs:
# Determine the next version, create the GitHub release and expose the tag.
bump-release:
uses: uug-ai/workflows/.github/workflows/release-bump.yml@main
permissions:
contents: write
# Verified revision supports reserving a tag with an unpublished draft.
uses: uug-ai/workflows/.github/workflows/release-bump.yml@f31d4260a833e82228ca0e9b9163231a970feeb8
with:
bump: ${{ github.event.inputs.bump }}
bump: ${{ inputs.bump }}
draft: true
secrets: inherit
# Publish the platform image to the uug-ai GitHub Container Registry
# (ghcr.io/uug-ai/agent-platform).
#release:
# needs: bump-release
# uses: uug-ai/workflows/.github/workflows/release-create.yml@main
# with:
# organization: uug-ai
# project: ${{ github.event.repository.name }}
# tag: ${{ needs.bump-release.outputs.tag }}
# docker_context: "."
# create_gitops_pr: false
# runner_matrix: >-
# [
# {"architecture":"amd64","runner":"ubuntu-24.04"},
# {"architecture":"arm64","runner":"ubuntu-24.04-arm"}
# ]
# secrets: inherit
# Everything below mirrors the agent's own release-create.yml pipeline and
# publishes the multi-arch image to the kerberos/agent Docker Hub repo, driven
# by the freshly bumped tag instead of a `release: created` event.
build-amd64:
release:
needs: bump-release
runs-on: ubuntu-24.04
permissions:
contents: write
strategy:
matrix:
architecture: [amd64]
steps:
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Checkout
uses: actions/checkout@v4
- name: Run Build
run: |
docker build --provenance=false --build-arg VERSION=${{ needs.bump-release.outputs.tag }} -t ${{matrix.architecture}} .
CID=$(docker create ${{matrix.architecture}})
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
docker rm ${CID}
- name: Strip binary
run: tar -cf agent-${{matrix.architecture}}.tar -C output-${{matrix.architecture}} . && rm -rf output-${{matrix.architecture}}
- name: Build and push Docker image
run: |
docker tag ${{matrix.architecture}} $REPO-arch:arch-${{matrix.architecture}}-${{ needs.bump-release.outputs.tag }}
docker push $REPO-arch:arch-${{matrix.architecture}}-${{ needs.bump-release.outputs.tag }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: agent-${{matrix.architecture}}.tar
path: agent-${{matrix.architecture}}.tar
build-arm64:
needs: bump-release
runs-on: ubuntu-24.04-arm
permissions:
contents: write
strategy:
matrix:
architecture: [arm64]
steps:
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Checkout
uses: actions/checkout@v4
- name: Run Build
run: |
docker build --provenance=false --build-arg VERSION=${{ needs.bump-release.outputs.tag }} -t ${{matrix.architecture}} .
CID=$(docker create ${{matrix.architecture}})
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
docker rm ${CID}
- name: Strip binary
run: tar -cf agent-${{matrix.architecture}}.tar -C output-${{matrix.architecture}} . && rm -rf output-${{matrix.architecture}}
- name: Build and push Docker image
run: |
docker tag ${{matrix.architecture}} $REPO-arch:arch-${{matrix.architecture}}-${{ needs.bump-release.outputs.tag }}
docker push $REPO-arch:arch-${{matrix.architecture}}-${{ needs.bump-release.outputs.tag }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: agent-${{matrix.architecture}}.tar
path: agent-${{matrix.architecture}}.tar
create-manifest:
runs-on: ubuntu-24.04
needs: [bump-release, build-amd64, build-arm64]
steps:
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Create and push multi-arch manifest
run: |
docker manifest create $REPO:${{ needs.bump-release.outputs.tag }} \
$REPO-arch:arch-amd64-${{ needs.bump-release.outputs.tag }} \
$REPO-arch:arch-arm64-${{ needs.bump-release.outputs.tag }}
docker manifest push $REPO:${{ needs.bump-release.outputs.tag }}
- name: Create and push latest manifest
run: |
docker manifest create $REPO:latest \
$REPO-arch:arch-amd64-${{ needs.bump-release.outputs.tag }} \
$REPO-arch:arch-arm64-${{ needs.bump-release.outputs.tag }}
docker manifest push $REPO:latest
create-release:
runs-on: ubuntu-24.04
needs: [bump-release, build-amd64, build-arm64]
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Create a release
uses: ncipollo/release-action@v1
with:
latest: true
allowUpdates: true
name: ${{ needs.bump-release.outputs.tag }}
tag: ${{ needs.bump-release.outputs.tag }}
generateReleaseNotes: false
omitBodyDuringUpdate: true
artifacts: "agent-*.tar/agent-*.tar"
id-token: write
uses: ./.github/workflows/image-release.yml
with:
tag: ${{ needs.bump-release.outputs.tag }}
source_ref: ${{ format('refs/tags/{0}', needs.bump-release.outputs.tag) }}
publish_latest: true
secrets: inherit
+12 -109
View File
@@ -5,118 +5,21 @@ on:
workflow_dispatch:
inputs:
tag:
description: "Tag for the Docker image"
description: Tag for the Docker image
required: true
default: "test"
default: test
env:
REPO: kerberos/agent
permissions:
contents: read
jobs:
build-amd64:
runs-on: ubuntu-24.04
release:
permissions:
contents: write
strategy:
matrix:
architecture: [amd64]
steps:
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Checkout
uses: actions/checkout@v4
- name: Run Build
run: |
docker build --provenance=false --build-arg VERSION=${{github.event.inputs.tag || github.ref_name}} -t ${{matrix.architecture}} .
CID=$(docker create ${{matrix.architecture}})
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
docker rm ${CID}
- name: Strip binary
run: tar -cf agent-${{matrix.architecture}}.tar -C output-${{matrix.architecture}} . && rm -rf output-${{matrix.architecture}}
- name: Build and push Docker image
run: |
docker tag ${{matrix.architecture}} $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}}
docker push $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: agent-${{matrix.architecture}}.tar
path: agent-${{matrix.architecture}}.tar
build-arm64:
runs-on: ubuntu-24.04-arm
permissions:
contents: write
strategy:
matrix:
architecture: [arm64]
steps:
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Checkout
uses: actions/checkout@v4
- name: Run Build
run: |
docker build --provenance=false --build-arg VERSION=${{github.event.inputs.tag || github.ref_name}} -t ${{matrix.architecture}} .
CID=$(docker create ${{matrix.architecture}})
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
docker rm ${CID}
- name: Strip binary
run: tar -cf agent-${{matrix.architecture}}.tar -C output-${{matrix.architecture}} . && rm -rf output-${{matrix.architecture}}
- name: Build and push Docker image
run: |
docker tag ${{matrix.architecture}} $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}}
docker push $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: agent-${{matrix.architecture}}.tar
path: agent-${{matrix.architecture}}.tar
create-manifest:
runs-on: ubuntu-24.04
needs: [build-amd64, build-arm64]
steps:
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Create and push multi-arch manifest
run: |
docker manifest create $REPO:${{ github.event.inputs.tag || github.ref_name }} \
$REPO-arch:arch-amd64-${{github.event.inputs.tag || github.ref_name}} \
$REPO-arch:arch-arm64-${{github.event.inputs.tag || github.ref_name}}
docker manifest push $REPO:${{ github.event.inputs.tag || github.ref_name }}
- name: Create and push latest manifest
run: |
docker manifest create $REPO:latest \
$REPO-arch:arch-amd64-${{github.event.inputs.tag || github.ref_name}} \
$REPO-arch:arch-arm64-${{github.event.inputs.tag || github.ref_name}}
docker manifest push $REPO:latest
if: github.event.inputs.tag == 'test'
create-release:
runs-on: ubuntu-24.04
needs: [build-amd64, build-arm64]
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Create a release
uses: ncipollo/release-action@v1
with:
latest: true
allowUpdates: true
name: ${{ github.event.inputs.tag || github.ref_name }}
tag: ${{ github.event.inputs.tag || github.ref_name }}
generateReleaseNotes: false
omitBodyDuringUpdate: true
artifacts: "agent-*.tar/agent-*.tar"
id-token: write
uses: ./.github/workflows/image-release.yml
with:
tag: ${{ inputs.tag || github.event.release.tag_name }}
source_ref: ${{ github.event_name == 'release' && format('refs/tags/{0}', github.event.release.tag_name) || github.sha }}
publish_latest: ${{ github.event_name == 'workflow_dispatch' && inputs.tag == 'test' }}
secrets: inherit
+42 -39
View File
@@ -1,6 +1,6 @@
ARG GO_IMAGE=golang:1.25-trixie
ARG RUNTIME_IMAGE=debian:trixie-slim
ARG GO_IMAGE=golang:1.26.10-trixie@sha256:a050dcceee6eb9bf110151662dfac7662f3bec40927c7dee48731e3d92083d42
ARG RUNTIME_IMAGE=debian:trixie-slim@sha256:f6b08b09a05725f9431502d3b5714f5c4576e82c1a84fa1debe0c24a7e0c3ec4
ARG VERSION=0.0.0
FROM ${GO_IMAGE} AS build-machinery
LABEL AUTHOR=uug.ai
@@ -14,12 +14,13 @@ ARG TARGETARCH
ENV GOROOT=/usr/local/go
ENV GOPATH=/go
ENV PATH=$GOPATH/bin:$GOROOT/bin:/usr/local/lib:$PATH
ENV GOSUMDB=off
ENV GOTOOLCHAIN=local
ENV CGO_ENABLED=1
##########################################
# Installing some additional dependencies.
RUN apt-get update && apt-get install -y --fix-missing --no-install-recommends \
RUN apt-get update && apt-get install -y --no-install-recommends \
git build-essential cmake pkg-config unzip libgtk2.0-dev \
curl ca-certificates libavcodec-dev libavutil-dev libcurl4-openssl-dev \
libssl-dev libjpeg62-turbo-dev libswscale-dev && \
@@ -31,13 +32,12 @@ RUN apt-get update && apt-get install -y --fix-missing --no-install-recommends \
RUN mkdir -p /go/src/github.com/kerberos-io/agent
WORKDIR /go/src/github.com/kerberos-io/agent/machinery
COPY machinery/go.mod machinery/go.sum ./
RUN go mod download
RUN go version && go mod download && go mod verify
##############################################################################
# Copy the rest of the source after dependencies are primed.
COPY machinery ./
RUN rm -rf .env
##################
# Build Machinery
@@ -46,12 +46,13 @@ RUN RESOLVED_VERSION="${VERSION:-0.0.0}" && \
printf '%s' "${RESOLVED_VERSION}" > version && \
BUILD_TAGS=timetzdata,netgo,osusergo && \
case "${TARGETARCH:-$(go env GOARCH)}" in amd64|arm64) BUILD_TAGS="moq,${BUILD_TAGS}" ;; esac && \
go build -o main -tags "${BUILD_TAGS}" --ldflags "-s -w -X github.com/kerberos-io/agent/machinery/src/utils.VERSION=${RESOLVED_VERSION}" . && \
go build -mod=readonly -trimpath -buildvcs=false -o main -tags "${BUILD_TAGS}" --ldflags "-s -w -X github.com/kerberos-io/agent/machinery/src/utils.VERSION=${RESOLVED_VERSION}" . && \
mkdir -p /agent && \
mv main /agent && \
mv version /agent && \
mv data /agent && \
mkdir -p /agent/data/cloud && \
mkdir -p /agent/data/tus && \
mkdir -p /agent/data/snapshots && \
mkdir -p /agent/data/log && \
mkdir -p /agent/data/recordings && \
@@ -70,7 +71,7 @@ RUN cp -r /agent ./
RUN /dist/agent/main version
FROM node:22-alpine AS build-ui
FROM node:22-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914 AS build-ui
RUN apk update && apk upgrade --available && sync
@@ -94,59 +95,61 @@ RUN mkdir -p ./agent && cp -r /go/src/github.com/kerberos-io/agent/machinery/www
############################################
# Publish main binary to GitHub release
FROM ${RUNTIME_IMAGE}
############################
# Protect by non-root user.
FROM ${RUNTIME_IMAGE} AS runtime-rootfs
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl ffmpeg libatomic1 libcap2-bin libstdc++6 && \
rm -rf /var/lib/apt/lists/* && \
groupadd --system kerberosio && \
useradd --system --gid kerberosio --groups video --create-home agent && \
chmod 0755 /home/agent
ca-certificates ffmpeg libatomic1 libstdc++6 \
dash coreutils findutils grep sed tzdata ncurses-base && \
groupadd --system --gid 999 kerberosio && \
useradd --system --uid 999 --gid kerberosio --groups video \
--home-dir /home/agent --no-create-home agent
#################################
# Copy files from previous images
COPY --chown=0:0 --from=build-machinery /dist /
COPY --chown=0:0 --from=build-ui /dist /
COPY --chown=0:0 --from=build-machinery /dist/agent/ /runtime/home/agent/
COPY --chown=0:0 --from=build-ui /dist/agent/ /runtime/home/agent/
COPY deployments/docker/assemble-rootfs.sh /usr/local/bin/assemble-rootfs
##################
# Try running agent
RUN chmod 0755 /runtime/home/agent /runtime/home/agent/main && \
cp /runtime/home/agent/data/config/config.json /runtime/home/agent/data/config.template.json && \
chown -R agent:kerberosio /runtime/home/agent/data && \
sh /usr/local/bin/assemble-rootfs
RUN mv /agent/* /home/agent/
RUN /home/agent/main version
FROM scratch
#######################
# Make template config
COPY --from=runtime-rootfs /runtime/ /
RUN cp /home/agent/data/config/config.json /home/agent/data/config.template.json
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ENV HOME=/home/agent
ENV XDG_CACHE_HOME=/tmp/agent-cache
ENV AGENT_PORT=8080
###########################
# Set permissions correctly
RUN chown -R agent:kerberosio /home/agent/data
RUN chown -R agent:kerberosio /home/agent/www
###########################
# Grant the necessary root capabilities to the process trying to bind to the privileged port
RUN setcap 'cap_net_bind_service=+ep' /home/agent/main
ARG VERSION
ARG REVISION=unknown
ARG CREATED
LABEL org.opencontainers.image.title="Kerberos Agent" \
org.opencontainers.image.description="Camera and media agent with a minimal dynamic scratch runtime" \
org.opencontainers.image.source="https://github.com/kerberos-io/agent" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.revision="${REVISION}" \
org.opencontainers.image.created="${CREATED}" \
org.opencontainers.image.vendor="UUG.AI"
###################
# Run non-root user
USER agent
USER 999:999
######################################
# By default the app runs on port 80
# The image uses an unprivileged HTTP port.
EXPOSE 80
EXPOSE 8080/tcp
######################################
# Check if agent is still running
HEALTHCHECK CMD curl --fail "http://localhost:${AGENT_PORT:-80}/health" || exit 1
HEALTHCHECK --timeout=10s CMD ["/home/agent/main", "-action", "healthcheck"]
###################################################
# Leeeeettttt'ssss goooooo!!!
+55 -11
View File
@@ -134,12 +134,51 @@ lists and automatic cleanup.
The easiest way to get your Kerberos Agent up and running is to use our public image on [Docker hub](https://hub.docker.com/r/kerberos/agent). Once you have selected a specific tag, run `docker` command below, which will open the web interface of your Kerberos agent on port `80`, and off you go. For a more configurable and persistent deployment have a look at [Running and automating a Kerberos Agent](#running-and-automating-a-kerberos-agent).
docker run -p 80:80 --name mycamera -d --restart=always kerberos/agent:latest
**Forthcoming hardened image** (use this mapping only after upgrading to a
release containing these changes):
docker run -p 80:8080 --name mycamera -d --restart=always kerberos/agent:latest
The hardened image listens on **8080 inside the container**, not 80. The mapping
`-p 80:8080` keeps the browser URL `http://localhost/` unchanged. When upgrading,
change the container side of existing `80:80` or `8082:80` mappings to
`80:8080` or `8082:8080`. Kubernetes Services must use `targetPort: 8080`.
Older published images still use their original port: these examples require a
release containing the hardening changes; pin its published digest in production.
For the currently published `v3.14.1`, retain the old mapping:
docker run -p 80:80 --name mycamera -d --restart=always kerberos/agent:v3.14.1
The image uses a Go 1.26 scratch runtime with only required dynamic/media
dependencies, numeric non-root UID/GID `999:999`, no file capabilities or
setuid/setgid programs, and an application healthcheck (no curl-based probe).
UI assets are immutable; demo mode selects its environment without rewriting them.
Source builds require the patched Go 1.26.10 toolchain or newer.
It supports dropping all capabilities and preventing privilege escalation:
```sh
docker run --cap-drop=ALL --security-opt=no-new-privileges:true \
-p 80:8080 --name mycamera -d kerberos/agent:latest
```
See the [image contract](deployments/docker/README.md#image-contract) for writable
paths, health checks, supported architectures, signature/evidence requirements,
and release-policy gaps. Functional remote-shell support remains a **pending
policy exception**, so this is not a claim of full image-hardening baseline or
CIS compliance.
Release promotion is also blocked by remaining native-library scan findings;
this PR is not release-qualified. The existing recording-signing key and trust
behavior are retained under the user's narrowly scoped exception through
December 5, 2026 (23:59:59 UTC), recorded for review in this PR. The finding
remains visible in scan reports; other keys and findings are not exempted.
If you want to connect to a USB or Raspberry Pi camera, [you'll need to run our side car container](https://github.com/kerberos-io/camera-to-rtsp) which proxies the camera to an RTSP stream. In that case you'll want to configure the Kerberos Agent container to run in the host network, so it can connect directly to the RTSP sidecar.
docker run --network=host --name mycamera -d --restart=always kerberos/agent:latest
With host networking, no port mapping applies: the hardened Agent is reachable
on host port `8080` by default.
## Quickstart - Balena
Run Kerberos Agent with [Balena Cloud](https://www.balena.io/) super powers. Monitor your Kerberos Agent with seamless remote access, over the air updates, an encrypted public `https` endpoint and much more. Checkout our application `video-surveillance` on [Balena Hub](https://hub.balena.io/apps/2064752/video-surveillance), and create your first or fleet of Kerberos Agent(s).
@@ -168,7 +207,7 @@ This repository contains everything you'll need to know about our core product,
- Low memory and CPU usage.
- Simplified and modern user interface.
- Multi architecture (ARMv6, ARMv7, ARM64, AMD64)
- Multi architecture (AMD64/ARM64 release images; [legacy ARM build limitations](deployments/docker/README.md#building-and-checking-the-image)).
- Multi stream, for example recording in H265, live streaming and motion detection in H264.
- Multi camera support: IP Cameras (H264 and H265), USB cameras and Raspberry Pi Cameras [through a RTSP proxy](https://github.com/kerberos-io/camera-to-rtsp).
- Single camera per instance (e.g. one container per camera).
@@ -221,21 +260,26 @@ An example of how to mount a host directory is shown below using `docker`, but i
You attach a volume to your container by leveraging the `-v` option. To mount your own configuration file and recordings folder, run the following commands:
docker run -p 80:80 --name mycamera \
docker run -p 80:8080 --name mycamera \
-v $(pwd)/agent/config:/home/agent/data/config \
-v $(pwd)/agent/recordings:/home/agent/data/recordings \
-d --restart=always kerberos/agent:latest
More examples for each deployment and automation tool [can be found in the deployment section](https://github.com/kerberos-io/agent/tree/master/deployments). Be sure to verify the permissions of the directory/volume you are attaching. More information in [this issue](https://github.com/kerberos-io/agent/issues/80).
chmod -R 755 kerberos-agent/
chown 100:101 kerberos-agent/ -R
The current image runs as UID `999`, GID `999`. Give that user ownership of
the mounted directories (older image tags may use different IDs):
sudo chown -R 999:999 agent/config agent/recordings
See [container permissions and hardening](deployments/docker/README.md#container-permissions-and-hardening)
for reduced-capability deployment and migration guidance.
## Configure with environment variables
Next to attaching the configuration file, it is also possible to override the configuration with environment variables. This makes deploying with `docker compose` or `kubernetes` much easier and more scalable. Using this approach, we simplify automation through `ansible` and `terraform`.
docker run -p 80:80 --name mycamera \
docker run -p 80:8080 --name mycamera \
-e AGENT_NAME=mycamera \
-e AGENT_TIMEZONE=Europe/Brussels \
-e AGENT_CAPTURE_IPCAMERA_RTSP=rtsp://fake.kerberos.io/stream \
@@ -358,7 +402,7 @@ See [RTSPS and TLS certificates](README-RTSPS-TLS.md) for the complete Bosch UI,
| --------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------ |
| `LOG_LEVEL` | Level for logging, could be "info", "warning", "debug", "error" or "fatal". | "info" |
| `LOG_OUTPUT` | Logging output format "json" or "text". | "text" |
| `AGENT_PORT` | HTTP web server port. Overrides the `-port` command-line value and must be between 1 and 65535. | "80" |
| `AGENT_PORT` | HTTP web server port. Overrides `-port`; use a port above 1024 in the hardened image. | "8080" hardened Docker; "80" legacy images/binaries |
| `AGENT_MODE` | You can choose to run this in 'release' for production, and or 'demo' for showcasing. | "release" |
| `AGENT_TLS_INSECURE` | Specify if you want to use `InsecureSkipVerify` for the internal HTTP client. | "false" |
| `AGENT_USERNAME` | The username used to authenticate against the Kerberos Agent login page. | "root" |
@@ -502,7 +546,7 @@ The value must be valid JSON on a single line, so wrap it in single quotes in a
shell:
```bash
docker run -p 80:80 --name mycamera \
docker run -p 80:8080 --name mycamera \
-e AGENT_CLOUD=kstorage \
-e AGENT_KERBEROSVAULT_URI=https://vault.domain.com/api \
-e AGENT_KERBEROSVAULT_ACCESS_KEY=xxx \
@@ -643,7 +687,7 @@ Go and open two terminals: one for the `ui` project and one for the `machinery`
1. Terminal A:
cd machinery/
go run main.go -action run -port 80
go run . -action run -port 80
2. Terminal B:
@@ -687,7 +731,7 @@ You can simply run the `machinery` using following commands.
git clone https://github.com/kerberos-io/agent
cd machinery
go run main.go -action run -port 8082
go run . -action run -port 8082
This will launch the Kerberos Agent and run a webserver on port `8082`, matching
the UI development default. You can change the port together with
@@ -732,7 +776,7 @@ The standard AMD64 and ARM64 images include the optional MoQ publisher. Its Rust
FFI archive requires CGO and glibc 2.38 or newer, which is why the standard image
uses Debian Trixie. The publisher is disabled unless explicitly enabled at runtime:
docker run --rm -p 80:80 \
docker run --rm -p 80:8080 \
-e AGENT_LIVE_MOQ_ENABLED=true \
-e AGENT_LIVE_MOQ_URL=https://relay.uug.ai/anon \
kerberos/agent
+11
View File
@@ -2,6 +2,17 @@
Kubernetes is great, but you might love OpenShift even more. In this directory you'll find some resources to deploy your Kerberos Agent in an OpenShift cluster using Ansible playbook. We'll review the different tasks of the Ansible playbook step by step; find the complete `playbook.yaml` here.
The playbook's port mapping targets the forthcoming hardened image:
`containerPort: 8080` and `targetPort: 8080`, with external Service port `80`
unchanged. Previously published images, including `v3.14.1`, still need the old
container port 80. Upgrade the image and mapping together and pin its published
digest. See the [image contract](../docker/README.md#image-contract) for numeric
UID/GID `999:999`, writable data/cache paths and release blockers. OpenShift SCCs
may override the UID; provision matching volume permissions and verify your
cluster's policy rather than granting privileged access. This legacy playbook's
credential-debugging/certificate-validation settings also require production
review; the image/port update does not certify the cluster deployment.
## Variabeles
We'll have a few `variables` in our `playbook.yml` that will help us to setup secure connection with the OpenShift cluster. We need the `cluster_url` and the `username` and `password` of the OpenShift cluster. If you don't know where to find this, you can find this in the OpenShift web ui.
+2 -2
View File
@@ -111,7 +111,7 @@
mountPath: /home/agent/data/cloud
subPath: cloud
ports:
- containerPort: 80
- containerPort: 8080
protocol: TCP
- name: Expose Kerberos Agent
@@ -136,5 +136,5 @@
ports:
- name: http
port: 80
targetPort: 80
targetPort: 8080
protocol: TCP
+247 -6
View File
@@ -4,17 +4,258 @@ The easiest, and let's say most natural, deployment is done [by utilising `docke
## 1. Running a single container
We are creating Docker images as part of our CI/CD process. You'll find our Docker images on [Docker hub](https://hub.docker.com/r/kerberos/agent). Pick a specific tag of choice, or use latest. Once done run below command, this will open the web interface of your Kerberos agent on port 80.
We publish production images on [Docker Hub](https://hub.docker.com/r/kerberos/agent).
The following examples target the forthcoming hardened image, which listens on
**8080 inside the container**. They preserve host port 80 and the browser URL
`http://localhost/`:
docker run -p 80:80 --name mycamera -d kerberos/agent:latest
docker run -p 80:8080 --name mycamera -d kerberos/agent:latest
Or for a develop build:
**Migration:** releases published before this change, including `v3.14.1`, still
listen on container port 80 and require `-p 80:80`. Do not change their mappings
until upgrading the image too. The separate `kerberos/agent-dev` image is not
migrated by this production-image change:
docker run -p 80:80 --name mycamera -d kerberos/agent-dev:latest
Feel free to use another port if your host system already has a workload running on `80`. For example `8082`.
docker run -p 8082:80 --name mycamera -d kerberos/agent:latest
docker run -p 8082:8080 --name mycamera -d kerberos/agent:latest
## Image contract
This contract describes the hardened production Dockerfile, not an already
published release or a claim of full
[image-hardening baseline](https://learn.uug.ai/guides/security/image-hardening/)
compliance. Use a release's actual published multi-platform digest, for example
`kerberos/agent:<version>@sha256:<digest>`, rather than treating mutable `latest`
as immutable. No new release digest or signature is asserted by this PR.
| Property | Contract |
|---|---|
| Identity | Fixed numeric `USER 999:999`, account `agent`, primary group `kerberosio`; not root and no privilege-changing startup. |
| Startup | Working directory `/home/agent`; exec-form default command `./main -action run`. Standalone binary CLI defaults remain unchanged. |
| Inbound HTTP | TCP `8080`, configurable with `AGENT_PORT` (overrides `-port`). Use an unprivileged port. Map host `80:8080`, or Service port `80` to `targetPort: 8080`. Terminate inbound TLS at a trusted proxy; the default listener is HTTP. |
| Privileges | No bundled setuid/setgid programs or file capabilities. Run with all capabilities dropped and `no-new-privileges`; no `NET_BIND_SERVICE` is required. |
| Writable application paths | Only `/home/agent/data/` (configuration, recordings, snapshots, cloud, tus checkpoints, capture tests and logs). Binary, UI assets and application home remain root-owned/read-only to UID 999. Demo mode serves its environment without rewriting UI assets. |
| Temporary state | `/tmp`, `/var/tmp`; user font/media caches under `XDG_CACHE_HOME=/tmp/agent-cache`, owned by `999:999` with mode `0700`. Recreate that directory with these permissions when mounting a fresh tmpfs over `/tmp`. |
| Health | Exec `/home/agent/main -action healthcheck`; five-second bounded localhost `/health` probe on the resolved port, no proxy/redirects, validates the Agent JSON envelope, exits nonzero on failure. Docker timeout is ten seconds. |
| Readiness and metrics | `/health` is HTTP liveness, not proof of camera, MQTT or Hub readiness. Camera/Hub details are diagnostic. This image does not add a Prometheus metrics listener. |
| Certificates/time | Debian CA bundle, OpenSSL configuration and timezone data retained. Mount a prepared CA bundle read-only and use `SSL_CERT_FILE` where appropriate for Go clients; native FFmpeg/OpenSSL clients may require their own CA configuration. Do not disable TLS verification as a substitute. |
| Secrets | Inject application credentials/keys at runtime using the orchestrator's existing environment/configuration mechanisms. Change default credentials before exposing HTTP. Never build production config, `.env`, private keys or registry credentials into an image. No new generic `*_FILE` interface is provided. |
| Outbound traffic | Configured RTSP/RTSPS/ONVIF camera endpoints, MQTT, Hub/Vault/storage HTTPS endpoints, DNS and optional telemetry; WebRTC may require configured STUN/TURN and negotiated UDP/TCP connectivity. Apply an allowlist suited to the actual enabled integrations. |
| Architecture | Native AMD64 and ARM64 build/runtime checks; ARMv7 remains a legacy build target needing its own evidence. ARMv6 is blocked by the default builder manifest; see below. |
### Container permissions and hardening
The final image uses `FROM scratch` with an explicitly assembled Debian/glibc
runtime rather than the whole Debian filesystem. FFmpeg and its required native
libraries/plugins remain for camera/media functionality, including AAC-to-PCMU.
Apt/dpkg executables, account/capability-management tools, curl, and bundled
setuid/setgid permissions are absent. An application subcommand, not shell/curl,
implements the healthcheck.
```sh
docker run --cap-drop=ALL --security-opt=no-new-privileges:true \
-p 80:8080 --name mycamera -d kerberos/agent:latest
```
Do not use `--privileged` for ordinary IP cameras. Local cameras still use the
separate RTSP sidecar; any device access belongs to that sidecar. Host networking
remains supported, but without a mapping the Agent is reachable on host port
`8080`, not 80.
The numeric `USER 999:999` does **not** automatically inherit supplementary video
membership from `/etc/group`. If a deployment deliberately passes a video device
to Agent, explicitly add the device's host GID: `--group-add 44` for GID 44,
Compose `group_add: ["44"]`, or Kubernetes `supplementalGroups: [44]`. The runtime
retains the `video` group record. Match the actual device permissions; this does
not grant arbitrary device access or replace the separate local-camera sidecar.
**Pending policy exception:** functional remote terminal sessions require
`/bin/sh`, PTY devices, terminal data and basic GNU utilities. These remain for
that feature, not healthchecks or debugging convenience. The user chose to
preserve the feature, but no policy approver, exception owner or expiry has been
provided. This is not an approved waiver or full baseline/CIS compliance.
Baseline-qualified release promotion must remain blocked until the exception is
formally resolved; do not silently remove the feature or manufacture approval.
Bind mounts must be writable by the image's UID/GID; the container does not
change host ownership at startup. Before migrating an older volume, check the
user of the exact old and new image with `docker run --rm --entrypoint id IMAGE`
and adjust only the mounted directories as necessary. For example:
```sh
sudo chown -R 999:999 agent/config agent/recordings
```
A read-only root filesystem is not enabled by default: `data/` needs initialized
writable storage (and FFmpeg or shell sessions may need `/tmp` and `/var/tmp`).
Empty bind mounts hide packaged configuration/media files; initialize
them before use. Docker named volumes populate packaged contents on first use.
Customers enabling read-only root must supply those writable mounts and cache
ownership, keep the runtime's device/PTY mounts, and validate their enabled media
and remote-access features. Apply CPU/memory/PID limits, seccomp/AppArmor and
network policies appropriate to the deployment.
### Building and checking the image
Build from a clean checkout: the default configuration files under
`machinery/data/config/` are included in the image, so do not replace them with
production credentials before building. `.dockerignore` excludes local `.env`
files, credential directories/configuration, test/profiling outputs, recordings,
upload checkpoints, dependency directories, and unrelated
repository files from the build context. The builder uses Go `1.26.10` with
`GOTOOLCHAIN=local`, checksum database verification, and `go mod verify`. Builds
use `-mod=readonly` to reject implicit module changes, `-trimpath` to omit local
source paths, and `-buildvcs=false` to omit checkout metadata. CGO remains enabled
for the native camera/media dependencies.
Source builds require at least Go 1.26.10, matching the pinned security patch floor.
The Go, Node, and Debian bases are pinned to verified multi-platform manifest
digests, including AMD64 and ARM64 (and ARMv7 where previously supported).
The pinned Debian image is used only to assemble the scratch runtime with
glibc-compatible FFmpeg, CGO, and MoQ dependencies. Its native package dependency
metadata selects the architecture's dynamic loader, shared libraries, media
plugins/drivers, and associated data files. This is intentionally broader than
an `ldd`-only copy, which misses dynamically loaded plugins. Certificates,
OpenSSL configuration, time zones, font/MIME/pixbuf caches, account records, and
standard writable `/tmp` and device mount points are retained. The runtime
uses `XDG_CACHE_HOME=/tmp/agent-cache` for disposable user caches without making
the application home writable. It contains package provenance in
`/var/lib/dpkg/status` and license notices, but
not the package manager. Update it by rebuilding and replacing the image rather
than installing packages in a running container.
```sh
docker build --pull -t kerberos/agent:local .
sh deployments/docker/test-image.sh kerberos/agent:local
```
The smoke check uses temporary containers and a Docker volume, cleans them up,
and checks permissions, FFmpeg audio transcoding, H264/sample-video decoding,
H265 encoding/decoding, font rendering, shell/PTY devices, volume
initialization/writes, default and custom HTTP ports, health probes, and
demo-mode startup with a read-only root and immutable UI assets. It enforces
privileged low ports while running on 8080 with no capabilities and
`no-new-privileges`, and also checks an overridden port `8082`. PR builds
run it on native AMD64 and ARM64 runners. It does not replace testing against a
real camera, hardware codecs/devices, an MQTT-mediated remote shell session, or
all supported ARM variants. Review and refresh the
base-image digests regularly for security updates; `--pull` alone does not update
a pinned digest. `GO_IMAGE` and `RUNTIME_IMAGE` remain overrideable build arguments
for compatible alternatives. The package repositories are not snapshot-pinned,
so base digest pins do not make the entire build bit-for-bit reproducible.
Architecture selections are unchanged by this hardening.
The pinned `golang:1.26.10-trixie` manifest, like the previous Go 1.25 trixie base,
has no `linux/arm/v6` variant, so the existing ARMv6 nightly target cannot be
validated with the default base.
ARMv6 needs a separately reviewed compatible builder/runtime combination; do
not assume an ARMv7 image runs on ARMv6 hardware.
### Release evidence, vulnerabilities and lifecycle
The hardened release path must build/test the supported release architectures,
block HIGH/CRITICAL vulnerability/secret/misconfiguration findings, retain scan
reports and an SBOM, publish build provenance, and sign the **final
multi-platform index digest**, not just one child manifest. A pending policy
exception is not an approved scan exception. There are no approved
vulnerability exceptions in this change; findings require remediation or a
reviewed owner/expiry/approval record. Scanner/database failures must fail
closed. Workflow definitions are not evidence that registry publication,
signing, or scans have already succeeded.
Source scans cover UI production **and build/development** dependencies and retain
their application inventory/SBOM, because compiled browser assets hide lockfile
metadata from final-rootfs scanners. Final-image evidence separately covers native
packages and the Go binary. Current native-library findings remain release
blockers. The existing embedded recording-signing key and trust behavior are
retained under the user's time-limited source-secret exception, recorded for
review in this PR in
[image-scan-exceptions.json](../../.github/scripts/image-scan-exceptions.json).
Owner and approver: `@cedricve`; expiry: **December 5, 2026, 23:59:59 UTC**.
Anyone possessing this shared private key can forge signatures under its
identity; the exception accepts that risk temporarily, it does not fix it.
The exception matches only that source file's SHA-256, exact scanner rule,
category, HIGH severity and line location, and requires exactly one match.
Changes, missing/incomplete approval, expiry or duplicate findings fail closed.
It cannot exempt an image finding, another secret, a vulnerability or a
misconfiguration. The policy is loaded from the executing workflow's checkout,
not the source directory being scanned. Retained `scan.json` still contains the
redacted HIGH finding; `exception-policy.json`, `exception-decisions.json` and
`policy.json` record the approval/application, without key material. Review the
record in PR #350 before merging; repository owners must enforce required reviews.
This exception does not approve the separate pending remote-terminal policy.
The build jobs run compilation and production-image smoke tests before the
security gates. A job that fails at **Source/UI dependency and final image gates
with SPDX SBOMs** is not a compiler or startup failure; inspect its retained
reports rather than disabling the gate. Patched Go dependencies are checked
with `govulncheck` as well as the image/source scanner. The pinned
`golang.org/x/crypto` revision is the official reviewed OpenPGP module split
for [GO-2026-5932](https://pkg.go.dev/vuln/GO-2026-5932); Agent does not import the
retired OpenPGP packages.
Debian 13 stable, security and backports currently provide no patched FFmpeg
7.1.x or libxml2 packages for the blocking findings on either AMD64 or ARM64.
For example, Debian defers [CVE-2026-58049](https://security-tracker.debian.org/tracker/CVE-2026-58049)
until an upstream 7.1.x fix; a fix exists in FFmpeg 9. A Wolfi FFmpeg 7 prototype
had no vendor-scanner findings but still used the same affected upstream
7.1.5 code and lacked the existing hardware accelerators. It was **not**
adopted to obtain a misleading clean scan. A newer media stack needs matched
CGO headers/libraries and validation of codecs, hardware acceleration and
supported architectures, not an unreviewed mix of testing-distribution packages.
The user has chosen to keep the default recording-signing key; no trust
migration or startup change is implemented. Its source-secret finding becomes
release-blocking again if the scoped exception expires or no longer matches.
Any future removal would require a separately
authorized migration: per-installation keys need verifier/public-key provisioning,
while requiring an injected key changes startup for existing keyless installations.
Before deploying, obtain the release's digest, source revision, supported
platforms, per-platform SBOM/scan reports, provenance, signature certificate
identity and policy approvals from its retained release evidence. Verify the
signature against the **exact trusted workflow identity** and GitHub OIDC issuer,
not an unrestricted identity regexp:
Workflow artifacts `image-source-evidence` and `image-final-evidence` retain
sanitized reports, source/application and per-platform SBOMs, provenance, base
digests and the source revision for 90 days. Successful releases also attach
`image-security-evidence.tar.gz`. After successful signing,
`signing-identity.txt` records the exact
`https://github.com/kerberos-io/agent/.github/workflows/image-release.yml@...`
identity; the suffix is the executing workflow ref, not necessarily the source
ref. Require the expected trusted ref, not merely any identity found in an
artifact. Secret matches/source excerpts are redacted; findings are not waived.
```sh
# Set these from the trusted release's real evidence, not from this example.
cosign verify --certificate-identity="$COSIGN_IDENTITY" \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
"$AGENT_IMAGE"
```
`AGENT_IMAGE` must contain the immutable registry digest. Reject missing,
mismatched, expired or unapproved evidence; neither a successful local build nor
`FROM scratch` certifies the image is vulnerability-free. Registry credentials,
OIDC permissions and repository release protections must be configured by the
repository owner; this PR does not invent them or certify those external controls.
Review pinned digests and native/package/module findings for every release and
regularly between releases. Debian 13 has
[full support until August 9, 2028 and LTS until June 30, 2030](https://www.debian.org/releases/trixie/),
with reduced LTS architecture/package coverage. The Go 1.26 line is supported
[until two newer major Go releases exist](https://go.dev/doc/devel/release#policy);
update its patch pin and replace it before that boundary. The build-only Node 22
line has [scheduled EOL April 30, 2027](https://github.com/nodejs/Release/blob/main/schedule.json).
The pinned UI builder is Node `22.23.4` on Alpine `3.24.3`;
[Alpine 3.24 main support ends June 1, 2028](https://alpinelinux.org/releases/),
while community support ends at the next stable release. Track both when
refreshing that build-only base. Rebuilding does not refresh immutable
base pins automatically, and APT repositories are not snapshot-pinned. Retain the
actual installed-package/SBOM evidence per architecture and release.
### Attach a volume
@@ -34,7 +275,7 @@ You attach a volume to your container by leveraging the `-v` option. To mount yo
3. Run the docker command as following to attach your config directory and recording directory.
docker run -p 80:80 --name mycamera \
docker run -p 80:8080 --name mycamera \
-v $(pwd)/agent/config:/home/agent/data/config \
-v $(pwd)/agent/recordings:/home/agent/data/recordings \
-d --restart=always kerberos/agent:latest
@@ -52,7 +293,7 @@ You'll find [an example `docker-compose.yaml` file here](https://github.com/kerb
kerberos-agent2:
image: "kerberos/agent:latest"
ports:
- "8082:80"
- "8082:8080"
environment:
- AGENT_NAME=agent2
- AGENT_CAPTURE_IPCAMERA_RTSP=rtsp://x.x.x.x:554/Streaming/Channels/101
+107
View File
@@ -0,0 +1,107 @@
#!/bin/sh
set -eu
root=/runtime
mkdir -p "$root/usr/bin" "$root/usr/sbin" "$root/usr/lib" "$root/etc" \
"$root/var/lib/dpkg" "$root/dev/pts" "$root/proc" "$root/sys" "$root/run"
ln -s usr/bin "$root/bin"
ln -s usr/sbin "$root/sbin"
ln -s usr/lib "$root/lib"
if [ -L /lib64 ]; then
mkdir -p "$root/usr/lib64"
ln -s usr/lib64 "$root/lib64"
fi
# Package dependencies include dlopen-loaded codecs/drivers and their data, which
# ldd alone cannot discover. Debian resolves the native architecture's loader too.
apt-cache depends --recurse --installed --no-recommends --no-suggests \
--no-conflicts --no-breaks --no-replaces --no-enhances \
ffmpeg libatomic1 libstdc++6 dash coreutils findutils grep sed tzdata ncurses-base \
> /tmp/runtime-dependencies
sed -n '/^[a-z0-9][a-z0-9+.:_-]*$/p' /tmp/runtime-dependencies \
| grep -vx debconf | sort -u > /tmp/runtime-candidates
dpkg-query -W -f='${Package} ${db:Status-Status}\n' > /tmp/package-status
awk '$2 == "installed" { print $1 }' /tmp/package-status \
| sort -u > /tmp/installed-packages
comm -12 /tmp/runtime-candidates /tmp/installed-packages > /tmp/runtime-packages
set --
while IFS= read -r package; do
set -- "$@" "$package"
done < /tmp/runtime-packages
dpkg-query -L "$@" > /tmp/runtime-package-paths
sort -u /tmp/runtime-package-paths > /tmp/runtime-paths
while IFS= read -r path; do
case "$path" in
"") continue ;;
/usr/share/doc/*/copyright|/usr/share/common-licenses/*) ;;
/usr/share/doc/*|/usr/share/man/*|/usr/share/info/*|/usr/share/locale/*|\
/usr/share/lintian/*|/usr/share/bug/*|/usr/share/debconf/*|\
/usr/share/bash-completion/*|/usr/share/fish/*|\
/usr/lib/systemd/*|/lib/systemd/*|/etc/init.d/*|/etc/cron.*/*) continue ;;
esac
if [ -d "$path" ] && [ ! -L "$path" ]; then
continue
fi
if [ ! -f "$path" ] && [ ! -L "$path" ]; then
printf 'Missing selected runtime file: %s\n' "$path" >&2
exit 1
fi
printf '%s\n' "$path"
done < /tmp/runtime-paths > /tmp/runtime-files
tar --create --file /tmp/runtime.tar --no-recursion --files-from /tmp/runtime-files
tar --extract --file /tmp/runtime.tar --directory "$root"
# Preserve generated trust stores, font configuration/caches and pixbuf loaders.
cp -a --parents /etc/ssl /usr/lib/ssl /usr/share/ca-certificates /etc/fonts \
/var/cache/fontconfig /usr/share/mime /usr/share/common-licenses "$root"
find /usr/lib -path '*/gdk-pixbuf-2.0/*/loaders.cache' \
-exec cp -a --parents {} "$root" \;
cp -a --parents /etc/nsswitch.conf /etc/gai.conf /etc/localtime \
/etc/ld.so.conf /etc/ld.so.conf.d /etc/os-release /etc/debian_version \
/usr/lib/os-release /usr/lib/locale "$root"
# Some public SONAME links (BLAS/LAPACK, for example) are generated by Debian
# alternatives rather than owned by a package. Keep only links to selected files.
find /usr/lib -type l > /tmp/runtime-links
if [ -d /usr/lib64 ]; then
find /usr/lib64 -type l >> /tmp/runtime-links
fi
while IFS= read -r link; do
target=$(readlink -f "$link")
if [ -e "$root$target" ]; then
while [ -L "$link" ]; do
cp -a --parents "$link" "$root"
target=$(readlink "$link")
case "$target" in
/*) link=$target ;;
*) link=$(dirname "$link")/$target ;;
esac
done
fi
done < /tmp/runtime-links
getent passwd root agent nobody > "$root/etc/passwd"
getent group root kerberosio video nogroup > "$root/etc/group"
mkdir -p "$root/tmp" "$root/var/tmp"
chmod 1777 "$root/tmp" "$root/var/tmp"
install -d -m 0700 -o 999 -g 999 "$root/tmp/agent-cache"
# Retain package provenance for scanners without shipping apt/dpkg executables.
dpkg-query -s "$@" ca-certificates base-files > "$root/var/lib/dpkg/status"
mkdir -p "$root/usr/share/doc/ca-certificates"
cp /usr/share/doc/ca-certificates/copyright "$root/usr/share/doc/ca-certificates/"
ldconfig -r "$root"
chroot "$root" /usr/bin/fc-cache --force
find "$root" -type f -perm /6000 -exec chmod a-s {} +
# Execute against the assembled loader/libraries, not the assembly-stage rootfs.
chroot --userspec=999:999 "$root" /bin/sh -ec '
/home/agent/main -action version
ffmpeg -version
ffprobe -version
test -s /etc/ssl/certs/ca-certificates.crt
test ! -e /usr/bin/curl
test ! -e /usr/bin/apt
test ! -e /usr/bin/dpkg
'
@@ -3,7 +3,7 @@ services:
kerberos-agent1:
image: "kerberos/agent:latest"
ports:
- "8081:80"
- "8081:8080"
environment:
- AGENT_NAME=agent1
# You can still override the configuration with environment variables, but might not makes sense if you are attaching a host config.
@@ -14,14 +14,14 @@ services:
kerberos-agent2:
image: "kerberos/agent:latest"
ports:
- "8082:80"
- "8082:8080"
volumes:
- ./agent2/config:/home/agent/data/config
- ./agent2/recordings:/home/agent/data/recordings
kerberos-agent3:
image: "kerberos/agent:latest"
ports:
- "8083:80"
- "8083:8080"
volumes:
- ./agent3/config:/home/agent/data/config
- ./agent3/recordings:/home/agent/data/recordings
+3 -3
View File
@@ -8,7 +8,7 @@ services:
kerberos-agent1:
image: "kerberos/agent:latest"
ports:
- "8081:80"
- "8081:8080"
environment:
<<: *common-variables
AGENT_NAME: agent1
@@ -17,7 +17,7 @@ services:
kerberos-agent2:
image: "kerberos/agent:latest"
ports:
- "8082:80"
- "8082:8080"
environment:
<<: *common-variables
AGENT_NAME: agent2
@@ -26,7 +26,7 @@ services:
kerberos-agent3:
image: "kerberos/agent:latest"
ports:
- "8083:80"
- "8083:8080"
environment:
<<: *common-variables
AGENT_NAME: agent3
+128
View File
@@ -0,0 +1,128 @@
#!/bin/sh
set -eu
image=${1:?Usage: sh deployments/docker/test-image.sh IMAGE}
container=
volume=
temporary_volume=
cleanup() {
if [ -n "$container" ]; then docker rm -f "$container" >/dev/null; fi
if [ -n "$volume" ]; then docker volume rm "$volume" >/dev/null; fi
if [ -n "$temporary_volume" ]; then docker volume rm "$temporary_volume" >/dev/null; fi
}
trap cleanup EXIT
trap 'exit 1' HUP INT TERM
docker run --rm --entrypoint /bin/sh "$image" -ec '
test "$(id -u)" = 999
test "$(id -g)" = 999
test ! -w /home/agent
test ! -w /home/agent/main
test -w /home/agent/data/config
test -w /home/agent/data/recordings
test -w /home/agent/data/tus
test ! -w /home/agent/www
test ! -w /home/agent/www/env.js
test -z "$(find / -xdev -type f -perm /6000 -print 2>/dev/null)"
! command -v setcap
! command -v capsh
! command -v apt
! command -v apt-get
! command -v dpkg
! command -v curl
test -s /var/lib/dpkg/status
test -r /etc/os-release
test -r /usr/share/terminfo/x/xterm-256color
test -s /etc/ssl/certs/ca-certificates.crt
test "$(id -Gn)" = "kerberosio"
test "$(stat -c %a /tmp)" = 1777
test -w "$XDG_CACHE_HOME"
test ! -e /home/agent/.env
test -z "$(find /home/agent/data/tus -type f -print)"
./main -action version
ffmpeg -nostdin -v error -f lavfi -i sine=frequency=1000:sample_rate=8000 \
-t 0.1 -c:a aac -f adts /tmp/test.aac
ffmpeg -nostdin -v error -i /tmp/test.aac -ar 8000 -ac 1 \
-c:a pcm_mulaw -f mulaw /tmp/test.pcm
test -s /tmp/test.pcm
ffprobe -v error -show_entries stream=codec_name data/test-480p.mp4
ffmpeg -nostdin -v error -i data/test-480p.mp4 -frames:v 1 -f null -
ffmpeg -nostdin -v error -i data/test-480p.mp4 -frames:v 1 \
-c:v libx265 -x265-params pools=none:frame-threads=1:log-level=error \
-f hevc /tmp/test.hevc
ffmpeg -nostdin -v error -i /tmp/test.hevc -frames:v 1 -f null -
ffmpeg -nostdin -v error -f lavfi -i color=black:s=64x64 \
-vf drawtext=text=scratch:fontcolor=white -frames:v 1 -f null -
'
# Exercise the same shell and PTY devices used by remote-access sessions.
docker run --rm -t --entrypoint /bin/sh "$image" -ec '
test -t 0
test -t 1
test -c /dev/ptmx
test -d /dev/pts
stty size
printf "Shell and PTY ready\n"
'
docker run --rm --group-add 44 --entrypoint /bin/sh "$image" -ec '
test "$(id -Gn)" = "kerberosio video"
'
wait_healthy() {
attempt=0
while [ "$attempt" -lt 60 ]; do
status=$(docker inspect --format '{{.State.Health.Status}}' "$container")
if [ "$status" = healthy ]; then return; fi
if [ "$(docker inspect --format '{{.State.Running}}' "$container")" != true ]; then
break
fi
attempt=$((attempt + 1))
sleep 1
done
docker logs "$container"
docker inspect --format '{{json .State.Health}}' "$container"
echo "Agent did not become healthy" >&2
return 1
}
# Exercise the unprivileged default port without any capabilities.
volume=$(docker volume create)
container=$(docker run -d --cap-drop ALL --security-opt no-new-privileges=true \
--sysctl net.ipv4.ip_unprivileged_port_start=1024 \
--health-interval=1s --health-start-period=0s \
--mount "type=volume,source=$volume,target=/home/agent/data" "$image")
wait_healthy
docker exec "$container" sh -ec '
test "$AGENT_PORT" = 8080
./main -action healthcheck
test -r data/config/config.json
touch data/config/.permission-test data/recordings/.permission-test data/tus/.permission-test
rm data/config/.permission-test data/recordings/.permission-test data/tus/.permission-test
'
if docker exec -e AGENT_PORT=65534 "$container" ./main -action healthcheck; then
echo "Healthcheck incorrectly succeeded on a port with no Agent" >&2
exit 1
fi
docker stop --time 10 "$container" >/dev/null
docker rm "$container" >/dev/null
container=
# Demo UI selection must work with immutable assets and a read-only root.
temporary_volume=$(docker volume create)
container=$(docker run -d --read-only --cap-drop ALL \
--security-opt no-new-privileges=true \
--sysctl net.ipv4.ip_unprivileged_port_start=1024 \
--health-interval=1s --health-start-period=0s \
--mount "type=volume,source=$volume,target=/home/agent/data" \
--mount "type=volume,source=$temporary_volume,target=/tmp" \
-e AGENT_PORT=8082 -e AGENT_MODE=demo \
-e http_proxy=http://127.0.0.1:9 -e HTTP_PROXY=http://127.0.0.1:9 \
-e no_proxy= -e NO_PROXY= "$image")
wait_healthy
docker exec "$container" sh -ec '
test -f www/env.js
test -f www/env.demo.js
test ! -w www
ffmpeg -nostdin -v error -f lavfi -i color=black:s=64x64 \
-vf drawtext=text=readonly:fontcolor=white -frames:v 1 -f null -
'
echo "Scratch image permissions, media, shell/PTY, volume writes, default/custom ports, and demo startup passed."
+23 -2
View File
@@ -6,6 +6,27 @@ Kubernetes can help you to build a scalable, flexible and resilient deployment.
We've provided an example deployment `deployment-agent.yml` in this directory, which show case you have to create a deployment (and under the hood a pod), to run a Kerberos Agent workload.
These manifests target the forthcoming hardened image: **containerPort and
Service targetPort are 8080**, while external Service/Ingress port 80 stays
unchanged. Pin a published hardened release digest before production use.
Previously published port-80 images (including `v3.14.1`) require the old
container/target ports; upgrade the image and mapping together. Host-network
deployments use host port 8080 without Service port translation.
The examples use UID/GID `999:999`, `fsGroup: 999`, a read-only root filesystem,
no privilege escalation, all capabilities dropped and runtime-default seccomp.
A non-root init container copies missing packaged data to the writable volume
without overwriting existing files and initializes the user cache. Temporary
storage is ephemeral. The PVC example preserves the existing `config/`,
`recordings/`, `snapshots/` and `cloud/` directories under the same PVC root,
mounting that root at `/home/agent/data` to cover all writable data paths.
Ensure the storage driver honors fsGroup or pre-provision UID/GID permissions.
If deliberately passing video devices, add their actual GID with
`supplementalGroups`; ordinary IP cameras do not need that group or devices.
See the [image contract](../docker/README.md#image-contract), including the pending
remote-terminal policy exception and release blockers. These examples do not
assert a running cluster or security-policy approval.
## Create a Kerberos Agent deployment
It's always a best practices to isolate and structure your workloads in Kubernetes. To achieve this we are utilising the concept of namespaces. For this example we will create a new namespace `demo`.
@@ -61,8 +82,7 @@ Depending on where and how you are hosting the Kubernetes cluster, you may need
image: kerberos/agent:latest
volumeMounts:
- name: kerberos-data
mountPath: /home/agent/data/config
subPath: config
mountPath: /home/agent/data
...
## Expose with Ingress
@@ -76,6 +96,7 @@ In the first example `deployment-agent.yml` we are using a `LoadBalancer` to exp
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
...
An alternative to `LoadBalancer` is `Ingress`. By leveraging an ingress such as `ingress-nginx` or `traefik` you setup a gateway (single point of contact), through which all communication to your apps (services) will flow.
@@ -14,41 +14,56 @@ spec:
labels:
app: agent
spec:
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
fsGroup: 999
seccompProfile:
type: RuntimeDefault
volumes:
- name: kerberos-data
persistentVolumeClaim:
claimName: kerberos-data
- name: temporary
emptyDir: {}
initContainers:
- name: download-config
image: kerberos/agent:latest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: kerberos-data
mountPath: /home/agent/data/config
subPath: config
command:
[
"cp",
"/home/agent/data/config.template.json",
"/home/agent/data/config/config.json",
]
mountPath: /runtime-data
- name: temporary
mountPath: /runtime-tmp
command: ["/bin/sh", "-ec", "cp -R --update=none /home/agent/data/. /runtime-data/; install -d -m 0700 /runtime-tmp/agent-cache"]
containers:
- name: agent
image: kerberos/agent:latest
livenessProbe:
exec:
command: ["/home/agent/main", "-action", "healthcheck"]
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 10
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: kerberos-data
mountPath: /home/agent/data/config
subPath: config
- name: kerberos-data
mountPath: /home/agent/data/recordings
subPath: recordings
- name: kerberos-data
mountPath: /home/agent/data/snapshots
subPath: snapshots
- name: kerberos-data
mountPath: /home/agent/data/cloud
subPath: cloud
mountPath: /home/agent/data
- name: temporary
mountPath: /tmp
- name: temporary
mountPath: /var/tmp
ports:
- containerPort: 80
- containerPort: 8080
protocol: TCP
@@ -14,11 +14,55 @@ spec:
labels:
app: agent
spec:
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
fsGroup: 999
seccompProfile:
type: RuntimeDefault
volumes:
- name: data
emptyDir: {}
- name: temporary
emptyDir: {}
initContainers:
- name: initialize-data
image: kerberos/agent:latest
command: ["/bin/sh", "-ec", "cp -R --update=none /home/agent/data/. /runtime-data/; install -d -m 0700 /runtime-tmp/agent-cache"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: data
mountPath: /runtime-data
- name: temporary
mountPath: /runtime-tmp
containers:
- name: agent
image: kerberos/agent:latest
livenessProbe:
exec:
command: ["/home/agent/main", "-action", "healthcheck"]
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 10
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: data
mountPath: /home/agent/data
- name: temporary
mountPath: /tmp
- name: temporary
mountPath: /var/tmp
ports:
- containerPort: 80
- containerPort: 8080
protocol: TCP
resources:
limits:
@@ -48,7 +92,7 @@ spec:
#type: LoadBalancer
ports:
- port: 80
targetPort: 80
targetPort: 8080
selector:
app: agent
---
+47 -3
View File
@@ -14,11 +14,55 @@ spec:
labels:
app: agent
spec:
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
fsGroup: 999
seccompProfile:
type: RuntimeDefault
volumes:
- name: data
emptyDir: {}
- name: temporary
emptyDir: {}
initContainers:
- name: initialize-data
image: kerberos/agent:latest
command: ["/bin/sh", "-ec", "cp -R --update=none /home/agent/data/. /runtime-data/; install -d -m 0700 /runtime-tmp/agent-cache"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: data
mountPath: /runtime-data
- name: temporary
mountPath: /runtime-tmp
containers:
- name: agent
image: kerberos/agent:3.2.3
image: kerberos/agent:latest
livenessProbe:
exec:
command: ["/home/agent/main", "-action", "healthcheck"]
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 10
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: data
mountPath: /home/agent/data
- name: temporary
mountPath: /tmp
- name: temporary
mountPath: /var/tmp
ports:
- containerPort: 80
- containerPort: 8080
protocol: TCP
resources:
limits:
@@ -48,6 +92,6 @@ spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 80
targetPort: 8080
selector:
app: agent
+7
View File
@@ -4,6 +4,13 @@ If you are using Terraform as part of your DevOps stack, you might utilise it to
For this example we will install Kerberos Agent using `docker` on a remote `linux` machine. Therefore we'll make sure we have the `TelkomIndonesia/linux` provider initialised.
The example targets the forthcoming hardened image and maps the configured host
port (80 by default) to **container port 8080**. Pin a published hardened release
digest before deployment. Older releases, including `v3.14.1`, still require
container port 80; change the image and mapping together. See the
[image contract](../docker/README.md#image-contract) for runtime permissions,
writable paths and release-policy limitations.
terraform init
Once initialised you should see similar output:
+2 -2
View File
@@ -34,9 +34,9 @@ resource "linux_script" "install_docker" {
resource "linux_script" "install_docker_kerberos_agent" {
lifecycle_commands {
create = "docker pull $IMAGE:$VERSION && docker run -d -p $PORT:80 --name agent $IMAGE:$VERSION"
create = "docker pull $IMAGE:$VERSION && docker run -d -p $PORT:8080 --name agent $IMAGE:$VERSION"
read = "docker inspect agent"
update = "docker pull $IMAGE:$VERSION && docker rm agent --force && docker run -d -p $PORT:80 --name agent $IMAGE:$VERSION"
update = "docker pull $IMAGE:$VERSION && docker rm agent --force && docker run -d -p $PORT:8080 --name agent $IMAGE:$VERSION"
delete = "docker rm agent --force"
}
environment = {
+27 -26
View File
@@ -1,6 +1,6 @@
module github.com/kerberos-io/agent/machinery
go 1.25.0
go 1.26.10
replace google.golang.org/genproto => google.golang.org/genproto v0.0.0-20250519155744-55703ea1f237
@@ -14,7 +14,7 @@ require (
github.com/creack/pty v1.1.24
github.com/dromara/carbon/v2 v2.6.8
github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5
github.com/eclipse/paho.mqtt.golang v1.5.0
github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/elastic/go-sysinfo v1.15.3
github.com/gin-contrib/cors v1.7.5
github.com/gin-contrib/pprof v1.5.3
@@ -39,12 +39,13 @@ require (
github.com/swaggo/swag v1.16.4
github.com/tevino/abool v1.2.0
github.com/zaf/g711 v1.4.0
go.mongodb.org/mongo-driver v1.17.3
go.opentelemetry.io/otel v1.36.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0
go.opentelemetry.io/otel/sdk v1.36.0
go.opentelemetry.io/otel/trace v1.36.0
go.mongodb.org/mongo-driver v1.17.7
go.opentelemetry.io/otel v1.45.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0
go.opentelemetry.io/otel/sdk v1.45.0
go.opentelemetry.io/otel/trace v1.45.0
golang.org/x/net v0.62.0
)
require (
@@ -55,7 +56,8 @@ require (
github.com/bluenviron/mediacommon/v2 v2.9.2 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj v1.8.4 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
@@ -65,7 +67,7 @@ require (
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
@@ -77,11 +79,11 @@ require (
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/juju/errors v1.0.0 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/compress v1.18.7 // indirect
github.com/klauspost/cpuid v1.2.3 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/kylelemons/go-gypsy v1.0.0 // indirect
@@ -121,22 +123,21 @@ require (
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/ziutek/mymysql v1.5.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.45.0 // indirect
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
golang.org/x/arch v0.16.0 // indirect
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.40.0 // indirect
golang.org/x/crypto v0.58.1-0.20261009181718-2f1f834330b2 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.24.0 // indirect
golang.org/x/sys v0.49.0 // indirect
golang.org/x/text v0.43.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.47.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/grpc v1.72.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect
golang.org/x/tools v0.51.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect
google.golang.org/grpc v1.83.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.42.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+76 -35
View File
@@ -410,12 +410,15 @@ github.com/cedricve/go-onvif v0.0.0-20200222191200-567e8ce298f6 h1:bzFZYgZD5vf4P
github.com/cedricve/go-onvif v0.0.0-20200222191200-567e8ce298f6/go.mod h1:nBrjN2nMHendp0Cvb/6GaJ1v92Qv/kzqxWtNBnKJEK0=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
@@ -470,6 +473,8 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o=
github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/elastic/go-sysinfo v1.15.3 h1:W+RnmhKFkqPTCRoFq2VCTmsT4p/fwpo+3gKNQsn1XU0=
github.com/elastic/go-sysinfo v1.15.3/go.mod h1:K/cNrqYTDrSoMh2oDkYEMS2+a72GRxMvNP+GC+vRIlo=
github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI=
@@ -548,8 +553,11 @@ github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpx
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@@ -748,6 +756,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
github.com/hamba/avro/v2 v2.17.2/go.mod h1:Q9YK+qxAhtVrNqOhwlZTATLgLA8qxG2vtvkhK8fJ7Jo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -781,8 +791,9 @@ github.com/kerberos-io/onvif v1.2.3/go.mod h1:XSgEQXmEDjUQTbdXvsaRJt6Az8YPGj7L+j
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs=
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@@ -924,8 +935,9 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
@@ -1011,6 +1023,8 @@ go.einride.tech/aip v0.67.1/go.mod h1:ZGX4/zKw8dcgzdLsrvpOOGxfxI2QSk12SlP7d6c0/X
go.einride.tech/aip v0.68.1/go.mod h1:XaFtaj4HuA3Zwk9xoBtTWgNubZ0ZZXv9BZJCkuKuWbg=
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.mongodb.org/mongo-driver v1.17.7 h1:a9w+U3Vt67eYzcfq3k/OAv284/uUUkL0uP75VE5rCOU=
go.mongodb.org/mongo-driver v1.17.7/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -1019,8 +1033,9 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.28.0/go.mod h1:9BIqH22qyHWAiZxQh0whuJygro59z+nbMVuc7ciiGug=
go.opentelemetry.io/contrib/detectors/gcp v1.29.0/go.mod h1:GW2aWZNwR2ZxDLdv8OyC2G8zkRoQBuURgV7RPQgcPoU=
go.opentelemetry.io/contrib/detectors/gcp v1.31.0/go.mod h1:tzQL6E1l+iV44YFTkcAeNQqzXUiekSYP9jjJjXwEd00=
@@ -1059,12 +1074,18 @@ go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGi
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 h1:QBajQ2SrwQijzHyZbQlPsuIzpl/ll8DY6wPWsajeGcI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0/go.mod h1:08ZQLjrPLQ6R4kAXvuOvODEer5Yh4CoFvll5qB2BCI8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY=
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
@@ -1080,8 +1101,10 @@ go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzau
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
@@ -1093,16 +1116,20 @@ go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJC
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg=
go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ=
go.opentelemetry.io/otel/sdk/metric v1.30.0/go.mod h1:waS6P3YqFNzeP01kuo/MBBYqaoBJl7efRQHOaydhy1Y=
go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk=
@@ -1116,14 +1143,18 @@ go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
@@ -1169,8 +1200,10 @@ golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
golang.org/x/crypto v0.58.1-0.20261009181718-2f1f834330b2 h1:zGDaB+/Mus2mScKvZ23k+W9NAUbP56sNM9SptgvWI5A=
golang.org/x/crypto v0.58.1-0.20261009181718-2f1f834330b2/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1240,8 +1273,8 @@ golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1329,8 +1362,8 @@ golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1380,8 +1413,9 @@ golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1410,8 +1444,8 @@ golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1520,8 +1554,8 @@ golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -1588,8 +1622,8 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1677,8 +1711,8 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1693,6 +1727,8 @@ gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJ
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
@@ -1866,8 +1902,10 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250425173222-7b384671a197/go.
google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw=
google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:W3S/3np0/dPWsWLi1h/UymYctGXaGBM2StwzD0y140U=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc=
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20231012201019-e917dd12ba7a/go.mod h1:+34luvCflYKiKylNwGJfn9cFBbcL/WrkciMmDmsTQ/A=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20231030173426-d783a09b4405/go.mod h1:GRUCuLdzVqZte8+Dl/D4N25yLzcGqqWaYkeVOwulFqw=
@@ -1986,8 +2024,10 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197/go.
google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -2062,8 +2102,8 @@ google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40Rmc
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y=
google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0=
@@ -2096,8 +2136,9 @@ google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+47
View File
@@ -0,0 +1,47 @@
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
func checkAgentHealth(ctx context.Context, port string) error {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1:"+port+"/health", nil)
if err != nil {
return fmt.Errorf("creating health request: %w", err)
}
client := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{DisableKeepAlives: true},
CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
},
}
defer client.CloseIdleConnections()
response, err := client.Do(request)
if err != nil {
return fmt.Errorf("requesting Agent health: %w", err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return fmt.Errorf("Agent health returned HTTP %d", response.StatusCode)
}
var health struct {
HTTPStatusCode int `json:"httpStatusCode"`
EntityStatusCode string `json:"entityStatusCode"`
Metadata struct {
ApplicationName string `json:"applicationName"`
} `json:"metadata"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 64*1024)).Decode(&health); err != nil {
return fmt.Errorf("decoding Agent health: %w", err)
}
if health.HTTPStatusCode != http.StatusOK || health.EntityStatusCode != "healthy" || health.Metadata.ApplicationName != "agent" {
return fmt.Errorf("Agent health returned an unexpected response")
}
return nil
}
+66
View File
@@ -0,0 +1,66 @@
package main
import (
"context"
"errors"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestCheckAgentHealth(t *testing.T) {
tests := []struct {
name string
status int
body string
wantOK bool
}{
{"healthy", 200, `{"httpStatusCode":200,"entityStatusCode":"healthy","metadata":{"applicationName":"agent"}}`, true},
{"unhealthy status", 503, `{}`, false},
{"redirect", 302, `{}`, false},
{"invalid JSON", 200, `not JSON`, false},
{"wrong service", 200, `{"httpStatusCode":200,"entityStatusCode":"healthy","metadata":{"applicationName":"other"}}`, false},
{"unhealthy body", 200, `{"httpStatusCode":200,"entityStatusCode":"unhealthy","metadata":{"applicationName":"agent"}}`, false},
}
t.Setenv("HTTP_PROXY", "http://127.0.0.1:1")
t.Setenv("NO_PROXY", "")
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/health" {
t.Errorf("path = %q, want /health", r.URL.Path)
}
w.Header().Set("Location", "/unexpected-redirect-target")
w.WriteHeader(test.status)
_, _ = w.Write([]byte(test.body))
}))
defer server.Close()
_, port, err := net.SplitHostPort(server.Listener.Addr().String())
if err != nil {
t.Fatal(err)
}
err = checkAgentHealth(context.Background(), port)
if (err == nil) != test.wantOK {
t.Fatalf("checkAgentHealth() = %v, wantOK %t", err, test.wantOK)
}
})
}
}
func TestCheckAgentHealthTimesOut(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
<-r.Context().Done()
}))
defer server.Close()
_, port, err := net.SplitHostPort(server.Listener.Addr().String())
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Millisecond)
defer cancel()
if err := checkAgentHealth(ctx, port); !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("healthcheck error = %v, want context deadline exceeded", err)
}
}
+9 -2
View File
@@ -96,7 +96,7 @@ func main() {
var timeout string
var subnet string
flag.StringVar(&action, "action", "version", "Tell us what you want do 'run' or 'version'")
flag.StringVar(&action, "action", "version", "Action: run, version, healthcheck, discover, or decrypt")
flag.StringVar(&configDirectory, "config", ".", "Where is the configuration stored")
flag.StringVar(&name, "name", "agent", "Provide a name for the agent")
flag.StringVar(&port, "port", "80", "On which port should the agent run")
@@ -117,7 +117,7 @@ func main() {
// Specify the timezone of the log: "UTC" or "Local".
timezone, _ := time.LoadLocation("CET")
configureLogging(logLevel, logOutput, timezone)
if action == "run" {
if action == "run" || action == "healthcheck" {
resolvedPort, err := resolveServerPort(port, os.Getenv("AGENT_PORT"))
if err != nil {
log.WithError(err).WithFields(log.Fields{
@@ -139,6 +139,13 @@ func main() {
switch action {
case "healthcheck":
if err := checkAgentHealth(context.Background(), port); err != nil {
log.WithError(err).WithFields(log.Fields{
"component": "healthcheck",
"event": "healthcheck_failed",
}).Fatal("Agent healthcheck failed")
}
case "version":
{
log.WithFields(log.Fields{
+19 -12
View File
@@ -83,18 +83,11 @@ func StartServer(configDirectory string, configuration *models.Configuration, co
// Add all routes
AddRoutes(r, authMiddleware, configDirectory, configuration, communication, captureDevice)
// Update environment variables
environmentVariables := configDirectory + "/www/env.js"
if os.Getenv("AGENT_MODE") == "demo" {
demoEnvironmentVariables := configDirectory + "/www/env.demo.js"
// Move demo environment variables to environment variables
err := os.Rename(demoEnvironmentVariables, environmentVariables)
if err != nil {
log.WithError(err).WithFields(log.Fields{
"component": "http",
"event": "demo_environment_move_failed",
}).Fatal("Failed to activate demo UI environment")
}
if err := addUIEnvironmentRoute(r, configDirectory); err != nil {
log.WithError(err).WithFields(log.Fields{
"component": "http",
"event": "ui_environment_unavailable",
}).Fatal("Failed to load UI environment")
}
// Add static routes to UI
@@ -123,6 +116,20 @@ func StartServer(configDirectory string, configuration *models.Configuration, co
}
}
func addUIEnvironmentRoute(router *gin.Engine, configDirectory string) error {
environmentFile := configDirectory + "/www/env.js"
if os.Getenv("AGENT_MODE") == "demo" {
environmentFile = configDirectory + "/www/env.demo.js"
}
if _, err := os.Stat(environmentFile); err != nil {
return fmt.Errorf("loading UI environment: %w", err)
}
router.Match([]string{"GET", "HEAD"}, "/env.js", func(c *gin.Context) {
c.File(environmentFile)
})
return nil
}
func requestLogger() gin.HandlerFunc {
return func(c *gin.Context) {
startedAt := time.Now()
@@ -0,0 +1,63 @@
package http
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/gin-gonic/gin"
)
func TestUIEnvironmentIsServedWithoutChangingAssets(t *testing.T) {
for _, mode := range []string{"", "demo"} {
t.Run(mode, func(t *testing.T) {
t.Setenv("AGENT_MODE", mode)
directory := t.TempDir()
www := filepath.Join(directory, "www")
if err := os.Mkdir(www, 0755); err != nil {
t.Fatal(err)
}
for name, content := range map[string]string{"env.js": "normal", "env.demo.js": "demo"} {
if err := os.WriteFile(filepath.Join(www, name), []byte(content), 0444); err != nil {
t.Fatal(err)
}
}
router := gin.New()
if err := addUIEnvironmentRoute(router, directory); err != nil {
t.Fatal(err)
}
want := "normal"
if mode == "demo" {
want = "demo"
}
for _, method := range []string{http.MethodGet, http.MethodHead} {
response := httptest.NewRecorder()
router.ServeHTTP(response, httptest.NewRequest(method, "/env.js", nil))
if response.Code != http.StatusOK {
t.Fatalf("%s /env.js: status %d", method, response.Code)
}
if method == http.MethodGet && response.Body.String() != want {
t.Fatalf("environment = %q, want %q", response.Body.String(), want)
}
if method == http.MethodHead && response.Body.Len() != 0 {
t.Fatal("HEAD returned a response body")
}
}
for name, want := range map[string]string{"env.js": "normal", "env.demo.js": "demo"} {
got, err := os.ReadFile(filepath.Join(www, name))
if err != nil || string(got) != want {
t.Fatalf("asset %s changed: %q, %v", name, got, err)
}
}
})
}
}
func TestUIEnvironmentRejectsMissingAsset(t *testing.T) {
t.Setenv("AGENT_MODE", "demo")
if err := addUIEnvironmentRoute(gin.New(), t.TempDir()); err == nil {
t.Fatal("missing demo environment was accepted")
}
}
+72
View File
@@ -24,11 +24,83 @@ See the section about [running tests](https://facebook.github.io/create-react-ap
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build script then replaces `../machinery/www` with the generated assets.
Run it in an isolated checkout or container when validating dependencies.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
## Dependency maintenance
Use the Node 22 image pinned in the root Dockerfile and Yarn Classic:
```sh
yarn install --frozen-lockfile
CI=true yarn test --watchAll=false --runInBand
yarn test:dependencies
yarn build
```
Keep the targeted `underscore` resolution: `jsonpath` pins a vulnerable version.
That resolution stays within the same major version. Axios remains on
its maintained 0.x line; `src/api/api.test.js` checks the HTTP behavior used by
the app without mocking Axios. Localization uses `i18next-xhr-backend`, not the
removed, unused `i18next-http-backend` dependency.
Additional targeted build-time upgrades are validated against the pinned
Node 22 / webpack 5 toolchain:
- [`resolve-url-loader` 5](https://github.com/bholloway/resolve-url-loader/releases/tag/5.0.0)
upgrades PostCSS to 8, drops Node <12 / webpack <4, and removes the deprecated
`rework` engine. CRA uses the supported PostCSS engine and unchanged `root` /
`sourceMap` options. Dependency tests check relative SCSS asset URLs, query
strings and fragments with source maps enabled and disabled.
- [`serialize-javascript` 7](https://github.com/yahoo/serialize-javascript/releases/tag/v7.0.0)
requires Node >=20, matching this UI's declared minimum. Its CommonJS serializer
API remains compatible with CSS minimizer and Rollup worker options.
Versions 5/6 changed sparse-array and URL serialization, neither of which these
callers use. Tests cover both consumers' function / regular-expression
serialization and the real Workbox Rollup minification worker; the production
build exercises CSS minimization. The targeted resolutions use patched 7.1.4.
- [`@svgr/webpack` 6](https://react-svgr.com/docs/migrate/) supports webpack 5 and
the existing `file-loader` combination. CRA disables SVGO and does not use a
custom template or call `@svgr/core`, so the documented breaking changes do
not affect this pipeline. Tests use CRA's actual SVG rule to check asset URLs,
React 17 rendering, viewBox preservation, titles and forwarded refs.
- [`webpack-dev-middleware` 7](https://github.com/webpack/webpack-dev-middleware/releases/tag/v7.0.0)
raises the Node minimum and upgrades its internal memory filesystem; version 6
raised the webpack minimum to 5. Both requirements match this toolchain, and
webpack-dev-server remains on CRA's 4.x line. Tests exercise real server
compilation, in-memory asset serving, missing assets and invalidation with
patched middleware 7.4.6. No build artifacts are written by this test.
Refreshing the lockfile does not resolve every legacy-toolchain advisory.
Create React App 5, `@kerberos-io/ui`'s SVG tooling, and their transitive
dependencies still require upstream upgrades or explicit, tested migrations.
Do not force incompatible transitive major versions just to clear a scan.
The obsolete `svg-react-loader` dependency declared by `@kerberos-io/ui` is
replaced with the maintained `@svgr/webpack` 8.1.0 through a Yarn package alias.
This removes its `css -> source-map-resolve -> decode-uri-component` chain;
it does not force an incompatible ESM version of the decoder into a CommonJS
caller. The shared UI ships precompiled SVG components and does not invoke that
loader. CRA's active SVG rule remains on the separately tested SVGR 6 path.
SVGR 8.1.0 is also pinned explicitly as a development dependency, so dependency
scanners inventory and check its actual package identity even if their Yarn
alias parser reports an additional `svg-react-loader@8.1.0` entry. Tests verify
that both names resolve to identical package metadata; the alias is not used
to conceal an unscanned replacement.
This is a scoped consumer migration, **not** a general drop-in implementation of
the old loader's `raw`, `filters`, `propsMap` or other query options. Dependency
tests guard against shared UI code starting to call the legacy API, resolve the
alias from the shared package, and compile/render its maintained replacement
with React 17, titles, forwarded refs, viewBox and caller props. Rendering is
compared to CRA's actual SVG pipeline. Recheck these assumptions on shared UI
upgrades; use SVGR's supported options for any new loader calls.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
+16 -2
View File
@@ -9,7 +9,7 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"axios": "^0.21.4",
"axios": "^0.35.0",
"classnames": "^2.3.1",
"connected-react-router": "^6.9.3",
"eslint-config-prettier": "8.5.*",
@@ -18,7 +18,6 @@
"history": "4.10.1",
"i18next": "^21.9.1",
"i18next-browser-languagedetector": "^6.1.5",
"i18next-http-backend": "^1.4.1",
"i18next-xhr-backend": "^3.2.2",
"md5": "^2.3.0",
"prettier": "^2.7.1",
@@ -41,6 +40,19 @@
"vott-ct": "^2.3.3",
"web-vitals": "^0.2.4"
},
"resolutions": {
"**/svg-react-loader": "npm:@svgr/webpack@8.1.0",
"**/jsonpath/underscore": "^1.13.8",
"**/resolve-url-loader": "^5.0.0",
"**/css-minimizer-webpack-plugin/serialize-javascript": "^7.1.4",
"**/rollup-plugin-terser/serialize-javascript": "^7.1.4",
"**/react-scripts/@svgr/webpack": "^6.5.1",
"**/@kerberos-io/ui/@svgr/webpack": "^6.5.1",
"**/webpack-dev-server/webpack-dev-middleware": "^7.4.6"
},
"engines": {
"node": ">=20"
},
"browser": {
"crypto": false
},
@@ -48,6 +60,7 @@
"start": "DISABLE_ESLINT_PLUGIN=true react-scripts start",
"build": "DISABLE_ESLINT_PLUGIN=true GENERATE_SOURCEMAP=false REACT_APP_ENVIRONMENT=production react-scripts build && rm -rf ../machinery/www && mv build ../machinery/www",
"test": "DISABLE_ESLINT_PLUGIN=true react-scripts test",
"test:dependencies": "node --test scripts/dependency-compatibility.test.cjs",
"eject": "react-scripts eject",
"lint": "eslint --debug 'src/**/*.{js,jsx,ts,tsx}'",
"format": "prettier --write \"**/*.{js,jsx,json,md}\""
@@ -65,6 +78,7 @@
]
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-plugin-import": "^2.26.0",
@@ -0,0 +1,261 @@
const assert = require('node:assert/strict');
const path = require('node:path');
const fs = require('node:fs');
const { createRequire } = require('node:module');
const { test } = require('node:test');
const vm = require('node:vm');
const requireFromCRA = createRequire(
require.resolve('react-scripts/package.json')
);
const resolveUrlLoader = requireFromCRA('resolve-url-loader');
const { SourceMapGenerator } = createRequire(
requireFromCRA.resolve('resolve-url-loader')
)('source-map');
for (const sourceMap of [false, true]) {
test(`SCSS URL resolution preserves nested assets and query strings (sourceMap=${sourceMap})`, async () => {
const root = path.resolve(__dirname, '../src');
const content =
'.fixture { background: url("../../header-minimal-logo-36x36.svg?theme=dark#icon"); }';
const map = new SourceMapGenerator({ file: path.join(root, 'App.scss') });
map.addMapping({
generated: { line: 1, column: 0 },
original: { line: 1, column: 0 },
source: path.join(root, 'components/LanguageSelect/LanguageSelect.scss'),
});
map.setSourceContent(
path.join(root, 'components/LanguageSelect/LanguageSelect.scss'),
content
);
const result = await new Promise((resolve, reject) => {
resolveUrlLoader.call(
{
resourcePath: path.join(root, 'App.scss'),
context: root,
rootContext: path.resolve(__dirname, '..'),
fs,
sourceMap,
query: { sourceMap, root },
getOptions() {
return this.query;
},
cacheable() {},
emitWarning: reject,
async() {
return (error, css, outputMap) => {
if (error) reject(error);
else resolve({ css, outputMap });
};
},
},
content,
map.toJSON()
);
});
assert.match(
result.css,
/url\("\.\/header-minimal-logo-36x36\.svg\?theme=dark#icon"\)/
);
assert.equal(Boolean(result.outputMap), sourceMap);
});
}
for (const parent of ['css-minimizer-webpack-plugin', 'rollup-plugin-terser']) {
test(`${parent} serializes worker options with functions and regular expressions`, () => {
const requireFromParent = createRequire(requireFromCRA.resolve(parent));
const serialize = requireFromParent('serialize-javascript');
const version = requireFromParent(
'serialize-javascript/package.json'
).version;
assert.match(version, /^7\./);
const options = {
comments: /^keep/,
minify: (input) => input.trim(),
nested: { passes: 2 },
};
const restored = vm.runInNewContext(`(${serialize(options)})`);
assert.equal(restored.comments.test('keep license'), true);
assert.equal(restored.comments.test('discard comment'), false);
assert.equal(restored.minify(' body '), 'body');
assert.equal(restored.nested.passes, 2);
});
}
test('Workbox Rollup minification passes serialized options through its real worker', async () => {
const { rollup } = requireFromCRA('rollup');
const { terser } = requireFromCRA('rollup-plugin-terser');
const bundle = await rollup({
input: 'compatibility-entry',
plugins: [
{
name: 'compatibility-fixture',
resolveId: (id) => (id === 'compatibility-entry' ? id : null),
load: () =>
'/*! keep license */ export const double = (value) => value * 2;',
},
terser({
numWorkers: 1,
mangle: false,
compress: { passes: 2 },
format: { comments: /keep/ },
}),
],
});
try {
const { output } = await bundle.generate({
format: 'cjs',
sourcemap: true,
});
const exports = {};
vm.runInNewContext(output[0].code, { exports });
assert.equal(exports.double(21), 42);
assert.match(output[0].code, /2\*value/);
assert.ok(output[0].map);
} finally {
await bundle.close();
}
});
test('shared UI ships precompiled SVGs without legacy loader calls', () => {
const packagePath = require.resolve('@kerberos-io/ui/package.json');
const requireFromUI = createRequire(packagePath);
const replacement = requireFromUI('svg-react-loader/package.json');
assert.equal(replacement.name, '@svgr/webpack');
assert.equal(replacement.version, '8.1.0');
assert.deepEqual(replacement, require('@svgr/webpack/package.json'));
assert.equal(requireFromCRA('@svgr/webpack/package.json').version, '6.5.1');
const lib = path.join(path.dirname(packagePath), 'lib');
const files = fs
.readdirSync(lib, { recursive: true })
.filter((file) => /\.[cm]?js$/.test(file));
assert.ok(files.length > 0);
for (const file of files) {
assert.doesNotMatch(
fs.readFileSync(path.join(lib, file), 'utf8'),
/svg-react-loader/,
`Shared UI now uses the legacy loader API in ${file}`
);
}
});
test('CRA and replacement SVG loaders preserve rendering through webpack-dev-server', async () => {
process.env.NODE_ENV = 'production';
const webpack = requireFromCRA('webpack');
const DevServer = requireFromCRA('webpack-dev-server');
const React = require('react');
const { renderToStaticMarkup } = require('react-dom/server');
const craConfig = requireFromCRA('./config/webpack.config.js')('production');
const svgRule = craConfig.module.rules
.find((rule) => rule.oneOf)
.oneOf.find(
(rule) => rule.test instanceof RegExp && rule.test.test('icon.svg')
);
assert.ok(svgRule);
const logoPath = path.resolve(
__dirname,
'../src/header-minimal-logo-36x36.svg'
);
const requireFromUI = createRequire(
require.resolve('@kerberos-io/ui/package.json')
);
const replacementRequest = `!!${requireFromUI.resolve(
'svg-react-loader'
)}?${JSON.stringify({
svgo: false,
titleProp: true,
ref: true,
})}!${logoPath}`;
const entry = `import url, { ReactComponent as Logo } from ${JSON.stringify(
logoPath
)}; import ReplacementLogo from ${JSON.stringify(
replacementRequest
)}; export { url, Logo, ReplacementLogo };`;
const compiler = webpack({
mode: 'development',
target: 'node',
entry: `data:text/javascript,${encodeURIComponent(entry)}`,
output: {
path: path.resolve(__dirname, '.dependency-webpack-output'),
filename: 'compatibility.js',
publicPath: '/compiled/',
library: { type: 'commonjs2' },
},
externals: { react: 'commonjs react' },
module: { rules: [{ ...svgRule, issuer: undefined }] },
infrastructureLogging: { level: 'error' },
stats: 'errors-only',
});
const server = new DevServer(
{
host: '127.0.0.1',
port: 0,
static: false,
hot: false,
client: false,
liveReload: false,
webSocketServer: false,
setupExitSignals: false,
devMiddleware: { publicPath: '/compiled/', stats: 'errors-only' },
},
compiler
);
try {
await server.start();
const stats = await new Promise((resolve) =>
server.middleware.waitUntilValid(resolve)
);
assert.equal(
stats.hasErrors(),
false,
stats.toString({ all: false, errors: true })
);
const baseURL = `http://127.0.0.1:${
server.server.address().port
}/compiled/`;
const response = await fetch(`${baseURL}compatibility.js`);
assert.equal(response.status, 200);
const module = { exports: {} };
vm.runInNewContext(await response.text(), {
module,
exports: module.exports,
require,
});
const { Logo, ReplacementLogo, url } = module.exports;
assert.equal(Logo.$$typeof, Symbol.for('react.forward_ref'));
assert.equal(ReplacementLogo.$$typeof, Symbol.for('react.forward_ref'));
const props = {
title: 'Camera icon',
titleId: 'camera-title',
className: 'camera-icon',
width: 48,
ref: React.createRef(),
};
const markup = renderToStaticMarkup(React.createElement(Logo, props));
assert.equal(
renderToStaticMarkup(React.createElement(ReplacementLogo, props)),
markup
);
assert.match(markup, /<svg/);
assert.match(markup, /<title id="camera-title">Camera icon<\/title>/);
assert.match(markup, /viewBox=/);
assert.match(markup, /<path/);
assert.match(url, /^\/compiled\/static\/media\/.*\.svg$/);
assert.equal((await fetch(new URL(url, baseURL))).status, 200);
assert.equal((await fetch(`${baseURL}missing.js`)).status, 404);
await new Promise((resolve) => server.middleware.invalidate(resolve));
assert.equal((await fetch(`${baseURL}compatibility.js`)).status, 200);
} finally {
await server.stop();
}
});
+65
View File
@@ -0,0 +1,65 @@
/* eslint-env jest */
import API from './api';
import config from '../config';
jest.mock('../config', () => ({
API_URL: 'http://localhost/api',
}));
describe('HTTP client compatibility', () => {
let originalAdapter;
let adapter;
beforeEach(() => {
originalAdapter = API.defaults.adapter;
adapter = jest.fn((request) => {
const response = {
config: request,
status: 200,
statusText: 'OK',
headers: {},
data: '{"ok":true}',
};
return Promise.resolve(response);
});
API.defaults.adapter = adapter;
localStorage.setItem('token', 'test-token');
});
afterEach(() => {
API.defaults.adapter = originalAdapter;
localStorage.clear();
});
it('preserves API URLs, bearer authentication, timeouts and JSON responses', async () => {
const response = await API.get('dashboard', { timeout: 15000 });
const request = adapter.mock.calls[0][0];
expect(request.baseURL).toBe(config.API_URL);
expect(request.url).toBe('dashboard');
expect(request.method).toBe('get');
expect(request.headers.Authorization).toBe('Bearer test-token');
expect(request.timeout).toBe(15000);
expect(response.data).toEqual({ ok: true });
});
it('serializes saved configuration as JSON and reads refreshed credentials', async () => {
localStorage.setItem('token', 'refreshed-token');
const settings = { capture: { ipcamera: { rtsp: 'rtsp://camera/live' } } };
await API.post('config', settings);
const request = adapter.mock.calls[0][0];
expect(request.method).toBe('post');
expect(request.headers.Authorization).toBe('Bearer refreshed-token');
expect(request.headers['Content-Type']).toMatch(/^application\/json/);
expect(JSON.parse(request.data)).toEqual(settings);
});
it('preserves rejected requests for API error handlers', async () => {
const error = new Error('Request timed out');
adapter.mockRejectedValueOnce(error);
await expect(API.get('dashboard', { timeout: 15000 })).rejects.toBe(error);
});
});
+4845 -3765
View File
File diff suppressed because it is too large Load Diff