mirror of
https://github.com/kerberos-io/agent.git
synced 2026-11-05 20:20:23 +00:00
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>
This commit is contained in:
co-authored by
Copilot App
parent
9553b1b4b9
commit
e3acd64a57
@@ -14,8 +14,18 @@ deployments/docker/*
|
||||
**/.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
|
||||
|
||||
@@ -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")
|
||||
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail-closed source/application and image scans with redacted evidence."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def check_report(report, image=False, source=False):
|
||||
if report.get("SchemaVersion") != 2 or not isinstance(report.get("Results"), list):
|
||||
raise ValueError("Missing or unsupported Trivy report")
|
||||
failures = []
|
||||
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 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")
|
||||
failures = check_report(report, image=bool(platform), source=not platform)
|
||||
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; no exclusions.",
|
||||
"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)
|
||||
try:
|
||||
failures = scan(args.target, args.output, args.platform)
|
||||
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": [], "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())
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# Release assets and these platform names were checked against v0.76.1.
|
||||
version=0.76.1
|
||||
case "$(uname -m)" in
|
||||
x86_64) platform=64bit ;;
|
||||
aarch64|arm64) platform=ARM64 ;;
|
||||
*) echo "Unsupported scanner host architecture" >&2; exit 1 ;;
|
||||
esac
|
||||
mkdir -p .ci-tools
|
||||
asset="trivy_${version}_Linux-${platform}.tar.gz"
|
||||
gh release download "v${version}" --repo aquasecurity/trivy \
|
||||
--pattern "$asset" --pattern "trivy_${version}_checksums.txt" --dir .ci-tools
|
||||
(
|
||||
cd .ci-tools
|
||||
grep " ${asset}$" "trivy_${version}_checksums.txt" | sha256sum --check --strict -
|
||||
tar -xzf "$asset" trivy
|
||||
)
|
||||
echo "$PWD/.ci-tools" >> "$GITHUB_PATH"
|
||||
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Focused release guard tests; no registry, signing, or scanner network access."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
import subprocess
|
||||
|
||||
|
||||
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:
|
||||
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 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()
|
||||
@@ -0,0 +1,326 @@
|
||||
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 verified scanner release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
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"
|
||||
@@ -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
|
||||
|
||||
@@ -1,38 +1,97 @@
|
||||
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: Test production image
|
||||
run: sh deployments/docker/test-image.sh ${{ matrix.architecture }}
|
||||
- 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
|
||||
- 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: Install scanner
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: bash .github/scripts/install-trivy.sh
|
||||
- name: Build once, populating OCI metadata from CI
|
||||
env:
|
||||
BUILDX_METADATA_PROVENANCE: max
|
||||
run: |
|
||||
mkdir -p evidence/build .ci-tools
|
||||
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
|
||||
# A local Docker load cannot retain attestations. The second exporter
|
||||
# retains attestations without registry writes. Never upload the full
|
||||
# image as scan evidence: it may contain a secret that correctly failed scans.
|
||||
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/build-metadata.json --tag "$ARCH" \
|
||||
--output type=docker --output type=oci,dest=.ci-tools/pr-image.oci.tar source
|
||||
jq -e '."buildx.build.provenance" | select(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
|
||||
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: 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: 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+18
-6
@@ -98,7 +98,7 @@ RUN mkdir -p ./agent && cp -r /go/src/github.com/kerberos-io/agent/machinery/www
|
||||
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 \
|
||||
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 \
|
||||
@@ -113,7 +113,7 @@ COPY deployments/docker/assemble-rootfs.sh /usr/local/bin/assemble-rootfs
|
||||
|
||||
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 /runtime/home/agent/www && \
|
||||
chown -R agent:kerberosio /runtime/home/agent/data && \
|
||||
sh /usr/local/bin/assemble-rootfs
|
||||
|
||||
FROM scratch
|
||||
@@ -123,21 +123,33 @@ COPY --from=runtime-rootfs /runtime/ /
|
||||
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
|
||||
|
||||
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 --timeout=10s CMD curl --noproxy '*' --fail --silent --show-error --max-time 5 "http://localhost:${AGENT_PORT:-80}/health" || exit 1
|
||||
HEALTHCHECK --timeout=10s CMD ["/home/agent/main", "-action", "healthcheck"]
|
||||
|
||||
###################################################
|
||||
# Leeeeettttt'ssss goooooo!!!
|
||||
|
||||
@@ -134,12 +134,48 @@ 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.
|
||||
Patched native Go dependencies now require Go 1.26 or newer for source builds.
|
||||
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/UI scan findings
|
||||
and the legacy default recording-signing key; this PR is not release-qualified.
|
||||
|
||||
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 +204,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,7 +257,7 @@ 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
|
||||
@@ -240,7 +276,7 @@ for reduced-capability deployment and migration guidance.
|
||||
|
||||
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 \
|
||||
@@ -363,7 +399,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" |
|
||||
@@ -507,7 +543,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 \
|
||||
@@ -648,7 +684,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:
|
||||
|
||||
@@ -692,7 +728,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
|
||||
@@ -737,7 +773,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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
+137
-34
@@ -4,47 +4,81 @@ 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 production image runs as `agent` (UID `999`, primary GID `999`), with the
|
||||
supplementary `video` group retained. The binary and home directory are owned by
|
||||
root; within `/home/agent`, only `data/` and `www/` are writable by the Agent. `www/` must remain writable
|
||||
because demo mode replaces `env.js` at startup. Existing `/home/agent/data/...`
|
||||
mount paths, the default port `80`, and the startup command are unchanged.
|
||||
|
||||
FFmpeg is required for AAC-to-PCMU audio transcoding, curl for the health check,
|
||||
and `/bin/sh` for remote-access sessions. They and the basic GNU shell utilities
|
||||
are intentionally retained. The final image uses `FROM scratch`, populated with
|
||||
an explicitly assembled Debian/glibc runtime rather than the whole Debian
|
||||
filesystem. Apt, dpkg executables, account-management and capability-management
|
||||
tools, and setuid/setgid permissions on bundled utilities are absent.
|
||||
The Agent binary retains only the file capability
|
||||
`NET_BIND_SERVICE`, needed for port `80` on hosts with privileged low ports.
|
||||
For an RTSP-only deployment, you can drop the other container capabilities:
|
||||
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 --cap-add=NET_BIND_SERVICE \
|
||||
-p 80:80 --name mycamera -d kerberos/agent:latest
|
||||
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. `no-new-privileges` prevents gaining the binary's file
|
||||
capability: if enabling it, use an unprivileged internal port (`AGENT_PORT=8082`)
|
||||
and ensure the container's `net.ipv4.ip_unprivileged_port_start` permits that
|
||||
port. Do not assume port `80` works with that option on every host.
|
||||
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
|
||||
@@ -55,22 +89,29 @@ and adjust only the mounted directories as necessary. For example:
|
||||
sudo chown -R 999:999 agent/config agent/recordings
|
||||
```
|
||||
|
||||
A read-only root filesystem is not enabled by default: both `data/` and `www/`
|
||||
need writable storage (and FFmpeg or shell sessions may need `/tmp`).
|
||||
Change the default application credentials before exposing the service.
|
||||
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, recordings, upload checkpoints, dependency directories, and unrelated
|
||||
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.
|
||||
|
||||
Patched Go dependencies require at least Go 1.26 for source builds.
|
||||
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
|
||||
@@ -95,9 +136,9 @@ 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. It enforces
|
||||
privileged low ports to check the file capability rather than relying on
|
||||
Docker's usual low-port exemption, and checks `no-new-privileges` on port `8082`. PR builds
|
||||
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
|
||||
@@ -113,6 +154,68 @@ 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/UI findings and the legacy
|
||||
embedded default recording-signing private key are still release blockers; the
|
||||
key's trust/migration behavior has not been silently changed or waived.
|
||||
|
||||
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
|
||||
|
||||
By default your Kerberos agent will store all its configuration and recordings inside the container. It might be interesting to store both configuration and your recordings outside the container, on your local disk. This helps persisting your storage even after you decide to wipe out your Kerberos agent.
|
||||
@@ -131,7 +234,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
|
||||
@@ -149,7 +252,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
|
||||
|
||||
@@ -16,7 +16,7 @@ fi
|
||||
# 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 \
|
||||
curl ffmpeg libatomic1 libstdc++6 dash coreutils findutils grep sed tzdata ncurses-base \
|
||||
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
|
||||
@@ -92,16 +92,14 @@ 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 {} +
|
||||
setcap 'cap_net_bind_service=+ep' "$root/home/agent/main"
|
||||
getcap "$root/home/agent/main"
|
||||
|
||||
# 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
|
||||
curl --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
|
||||
@@ -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
|
||||
|
||||
@@ -4,9 +4,11 @@ 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
|
||||
@@ -19,18 +21,20 @@ docker run --rm --entrypoint /bin/sh "$image" -ec '
|
||||
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
|
||||
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 video"
|
||||
test "$(id -Gn)" = "kerberosio"
|
||||
test "$(stat -c %a /tmp)" = 1777
|
||||
test -w "$XDG_CACHE_HOME"
|
||||
test ! -e /home/agent/.env
|
||||
@@ -60,6 +64,9 @@ docker run --rm -t --entrypoint /bin/sh "$image" -ec '
|
||||
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
|
||||
@@ -77,30 +84,45 @@ wait_healthy() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Exercise the default port, Docker volume initialization, and reduced capabilities.
|
||||
# Exercise the unprivileged default port without any capabilities.
|
||||
volume=$(docker volume create)
|
||||
container=$(docker run -d --cap-drop ALL --cap-add NET_BIND_SERVICE \
|
||||
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 mode must still be able to replace env.js; probes must bypass proxy settings.
|
||||
container=$(docker run -d --cap-drop ALL --cap-add NET_BIND_SERVICE \
|
||||
# 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 ! -e www/env.demo.js'
|
||||
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."
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
+20
-19
@@ -1,6 +1,6 @@
|
||||
module github.com/kerberos-io/agent/machinery
|
||||
|
||||
go 1.25.0
|
||||
go 1.26.0
|
||||
|
||||
replace google.golang.org/genproto => google.golang.org/genproto v0.0.0-20250519155744-55703ea1f237
|
||||
|
||||
@@ -40,11 +40,12 @@ require (
|
||||
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 v1.44.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.opentelemetry.io/otel/sdk v1.44.0
|
||||
go.opentelemetry.io/otel/trace v1.44.0
|
||||
golang.org/x/net v0.60.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -56,6 +57,7 @@ require (
|
||||
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/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.3 // 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
|
||||
@@ -81,7 +83,7 @@ require (
|
||||
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/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.6.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.57.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.23.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/text v0.42.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.49.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // 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
|
||||
|
||||
+45
-35
@@ -416,6 +416,7 @@ github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91
|
||||
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=
|
||||
@@ -548,8 +549,9 @@ 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/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=
|
||||
@@ -781,8 +783,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 +927,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=
|
||||
@@ -1019,8 +1023,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,8 +1064,8 @@ 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/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/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM=
|
||||
@@ -1080,8 +1085,8 @@ 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/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 +1098,17 @@ 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/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/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,8 +1122,8 @@ 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/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=
|
||||
@@ -1169,8 +1175,8 @@ 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.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
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 +1246,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 +1335,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.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
|
||||
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
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 +1386,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 +1417,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.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.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 +1527,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.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.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 +1595,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.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
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 +1684,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.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
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 +1700,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 +1875,8 @@ 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/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 +1995,8 @@ 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/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 +2071,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 +2105,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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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", "http://127.0.0.1:1")
|
||||
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); err == nil {
|
||||
t.Fatal("healthcheck succeeded despite its deadline")
|
||||
}
|
||||
}
|
||||
+9
-2
@@ -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{
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user