Compare commits
2 Commits
a10a01c903
...
3fc17701df
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fc17701df | |||
| 9788bdde0c |
@@ -21,23 +21,13 @@ jobs:
|
|||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
- name: Configure SSH for release dependencies
|
- name: Use anonymous HTTPS for public GovOPlaN repositories
|
||||||
env:
|
|
||||||
GOVOPLAN_RELEASE_SSH_KEY_B64: ${{ secrets.GOVOPLAN_RELEASE_SSH_KEY_B64 }}
|
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.ssh
|
git config --global --add url."https://git.add-ideas.de/add-ideas/govoplan".insteadOf "git@git.add-ideas.de:add-ideas/govoplan"
|
||||||
chmod 700 ~/.ssh
|
git config --global --add url."https://git.add-ideas.de/add-ideas/govoplan".insteadOf "ssh://git@git.add-ideas.de/add-ideas/govoplan"
|
||||||
if [ -z "${GOVOPLAN_RELEASE_SSH_KEY_B64:-}" ]; then
|
|
||||||
echo "GOVOPLAN_RELEASE_SSH_KEY_B64 secret is required for git+ssh release dependencies."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
printf '%s' "$GOVOPLAN_RELEASE_SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
|
|
||||||
chmod 600 ~/.ssh/id_ed25519
|
|
||||||
echo 'git.add-ideas.de ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDe48IOof2fJS1dTbJtLWQnWnr+JorZXKIFdOAM9ct8G' > ~/.ssh/known_hosts
|
|
||||||
chmod 600 ~/.ssh/known_hosts
|
|
||||||
- name: Bootstrap GovOPlaN repositories
|
- name: Bootstrap GovOPlaN repositories
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: python tools/repo/bootstrap-repositories.py --parent ..
|
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --exclude-repo addideas-govoplan-website
|
||||||
- name: Install backend dev audit dependencies
|
- name: Install backend dev audit dependencies
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -17,23 +17,13 @@ jobs:
|
|||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
- name: Configure SSH for release dependencies
|
- name: Use anonymous HTTPS for public GovOPlaN repositories
|
||||||
env:
|
|
||||||
GOVOPLAN_RELEASE_SSH_KEY_B64: ${{ secrets.GOVOPLAN_RELEASE_SSH_KEY_B64 }}
|
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.ssh
|
git config --global --add url."https://git.add-ideas.de/add-ideas/govoplan".insteadOf "git@git.add-ideas.de:add-ideas/govoplan"
|
||||||
chmod 700 ~/.ssh
|
git config --global --add url."https://git.add-ideas.de/add-ideas/govoplan".insteadOf "ssh://git@git.add-ideas.de/add-ideas/govoplan"
|
||||||
if [ -z "${GOVOPLAN_RELEASE_SSH_KEY_B64:-}" ]; then
|
|
||||||
echo "GOVOPLAN_RELEASE_SSH_KEY_B64 secret is required for git+ssh release dependencies."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
printf '%s' "$GOVOPLAN_RELEASE_SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
|
|
||||||
chmod 600 ~/.ssh/id_ed25519
|
|
||||||
echo 'git.add-ideas.de ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDe48IOof2fJS1dTbJtLWQnWnr+JorZXKIFdOAM9ct8G' > ~/.ssh/known_hosts
|
|
||||||
chmod 600 ~/.ssh/known_hosts
|
|
||||||
- name: Bootstrap GovOPlaN repositories
|
- name: Bootstrap GovOPlaN repositories
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: python tools/repo/bootstrap-repositories.py --parent ..
|
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --exclude-repo addideas-govoplan-website
|
||||||
- name: Install backend release dependencies
|
- name: Install backend release dependencies
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -16,23 +16,13 @@ jobs:
|
|||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
- name: Configure SSH for release dependencies
|
- name: Use anonymous HTTPS for public GovOPlaN repositories
|
||||||
env:
|
|
||||||
GOVOPLAN_RELEASE_SSH_KEY_B64: ${{ secrets.GOVOPLAN_RELEASE_SSH_KEY_B64 }}
|
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.ssh
|
git config --global --add url."https://git.add-ideas.de/add-ideas/govoplan".insteadOf "git@git.add-ideas.de:add-ideas/govoplan"
|
||||||
chmod 700 ~/.ssh
|
git config --global --add url."https://git.add-ideas.de/add-ideas/govoplan".insteadOf "ssh://git@git.add-ideas.de/add-ideas/govoplan"
|
||||||
if [ -z "${GOVOPLAN_RELEASE_SSH_KEY_B64:-}" ]; then
|
|
||||||
echo "GOVOPLAN_RELEASE_SSH_KEY_B64 secret is required for git+ssh release dependencies."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
printf '%s' "$GOVOPLAN_RELEASE_SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
|
|
||||||
chmod 600 ~/.ssh/id_ed25519
|
|
||||||
echo 'git.add-ideas.de ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDe48IOof2fJS1dTbJtLWQnWnr+JorZXKIFdOAM9ct8G' > ~/.ssh/known_hosts
|
|
||||||
chmod 600 ~/.ssh/known_hosts
|
|
||||||
- name: Bootstrap GovOPlaN repositories
|
- name: Bootstrap GovOPlaN repositories
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: python tools/repo/bootstrap-repositories.py --parent ..
|
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --exclude-repo addideas-govoplan-website
|
||||||
- name: Install backend release integration dependencies
|
- name: Install backend release integration dependencies
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
name: Security Audit
|
name: Security Audit
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -21,6 +20,10 @@ jobs:
|
|||||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
with:
|
with:
|
||||||
path: govoplan
|
path: govoplan
|
||||||
|
- name: Use anonymous HTTPS for public GovOPlaN repositories
|
||||||
|
run: |
|
||||||
|
git config --global --add url."https://git.add-ideas.de/add-ideas/govoplan".insteadOf "git@git.add-ideas.de:add-ideas/govoplan"
|
||||||
|
git config --global --add url."https://git.add-ideas.de/add-ideas/govoplan".insteadOf "ssh://git@git.add-ideas.de/add-ideas/govoplan"
|
||||||
- name: Configure SSH for repository bootstrap
|
- name: Configure SSH for repository bootstrap
|
||||||
env:
|
env:
|
||||||
GOVOPLAN_RELEASE_SSH_KEY_B64: ${{ secrets.GOVOPLAN_RELEASE_SSH_KEY_B64 }}
|
GOVOPLAN_RELEASE_SSH_KEY_B64: ${{ secrets.GOVOPLAN_RELEASE_SSH_KEY_B64 }}
|
||||||
@@ -37,8 +40,8 @@ jobs:
|
|||||||
chmod 600 ~/.ssh/known_hosts
|
chmod 600 ~/.ssh/known_hosts
|
||||||
- name: Bootstrap GovOPlaN repositories
|
- name: Bootstrap GovOPlaN repositories
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: python tools/repo/bootstrap-repositories.py --parent ..
|
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https
|
||||||
- name: Run security audit
|
- name: Run whole-system security audit
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: tools/checks/security-audit/run.sh --mode "$SECURITY_AUDIT_MODE" --scope "$SECURITY_AUDIT_SCOPE" --reports-dir audit-reports
|
run: tools/checks/security-audit/run.sh --mode "$SECURITY_AUDIT_MODE" --scope "$SECURITY_AUDIT_SCOPE" --reports-dir audit-reports
|
||||||
- name: Upload audit reports
|
- name: Upload audit reports
|
||||||
|
|||||||
@@ -162,7 +162,12 @@ clusters that cross module ownership or make behavior harder to change safely.
|
|||||||
|
|
||||||
The regular `Security Audit` workflow reuses the fingerprinted toolbox image
|
The regular `Security Audit` workflow reuses the fingerprinted toolbox image
|
||||||
when the Docker daemon is persistent, which is the normal case for the
|
when the Docker daemon is persistent, which is the normal case for the
|
||||||
self-hosted Gitea runner using the host Docker socket. The separate
|
self-hosted Gitea runner using the host Docker socket. Trusted push, schedule,
|
||||||
|
and manual runs scan all registered repositories; authenticated SSH is used
|
||||||
|
only for the private website repository. Pull-request audit runs stay disabled
|
||||||
|
while the audit runner exposes its host Docker socket: PR-controlled audit code
|
||||||
|
must run on a disposable or rootless runner without host-socket access. The
|
||||||
|
separate
|
||||||
`Security Audit Toolbox Update` workflow runs weekly with
|
`Security Audit Toolbox Update` workflow runs weekly with
|
||||||
`SECURITY_AUDIT_UPDATE=1`; it pulls current base images and re-resolves the
|
`SECURITY_AUDIT_UPDATE=1`; it pulls current base images and re-resolves the
|
||||||
allowed tool version ranges into a refreshed local image.
|
allowed tool version ranges into a refreshed local image.
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
{"name": "govoplan-templates", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-templates.git", "path": "govoplan-templates"},
|
{"name": "govoplan-templates", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-templates.git", "path": "govoplan-templates"},
|
||||||
{"name": "govoplan-tenancy", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-tenancy.git", "path": "govoplan-tenancy"},
|
{"name": "govoplan-tenancy", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-tenancy.git", "path": "govoplan-tenancy"},
|
||||||
{"name": "govoplan-transparency", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-transparency.git", "path": "govoplan-transparency"},
|
{"name": "govoplan-transparency", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-transparency.git", "path": "govoplan-transparency"},
|
||||||
{"name": "addideas-govoplan-website", "category": "website", "subtype": "public-site", "remote": "git@git.add-ideas.de:add-ideas/addideas-govoplan-website.git", "path": "addideas-govoplan-website"},
|
{"name": "addideas-govoplan-website", "category": "website", "subtype": "public-site", "remote": "git@git.add-ideas.de:add-ideas/addideas-govoplan-website.git", "path": "addideas-govoplan-website", "bootstrap_transport": "registered"},
|
||||||
{"name": "govoplan-workflow", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-workflow.git", "path": "govoplan-workflow"},
|
{"name": "govoplan-workflow", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-workflow.git", "path": "govoplan-workflow"},
|
||||||
{"name": "govoplan-xoev", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:add-ideas/govoplan-xoev.git", "path": "govoplan-xoev"},
|
{"name": "govoplan-xoev", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:add-ideas/govoplan-xoev.git", "path": "govoplan-xoev"},
|
||||||
{"name": "govoplan-xrechnung", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:add-ideas/govoplan-xrechnung.git", "path": "govoplan-xrechnung"},
|
{"name": "govoplan-xrechnung", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:add-ideas/govoplan-xrechnung.git", "path": "govoplan-xrechnung"},
|
||||||
|
|||||||
364
tests/test_repository_bootstrap.py
Normal file
364
tests/test_repository_bootstrap.py
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
META_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SCRIPT = META_ROOT / "tools" / "repo" / "bootstrap-repositories.py"
|
||||||
|
|
||||||
|
|
||||||
|
def load_bootstrap_module():
|
||||||
|
spec = importlib.util.spec_from_file_location("bootstrap_repositories", SCRIPT)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
raise RuntimeError(f"Could not load {SCRIPT}")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[spec.name] = module
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryBootstrapTests(unittest.TestCase):
|
||||||
|
def test_public_https_transport_rewrites_registered_gitea_remotes(self) -> None:
|
||||||
|
bootstrap = load_bootstrap_module()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
"https://git.add-ideas.de/add-ideas/govoplan-core.git",
|
||||||
|
bootstrap.clone_remote(
|
||||||
|
"git@git.add-ideas.de:add-ideas/govoplan-core.git",
|
||||||
|
transport=bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"https://git.add-ideas.de/add-ideas/govoplan-core.git",
|
||||||
|
bootstrap.clone_remote(
|
||||||
|
"ssh://git@git.add-ideas.de/add-ideas/govoplan-core.git",
|
||||||
|
transport=bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"https://git.add-ideas.de/add-ideas/govoplan-core.git",
|
||||||
|
bootstrap.clone_remote(
|
||||||
|
"https://git.add-ideas.de/add-ideas/govoplan-core.git",
|
||||||
|
transport=bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_registered_transport_preserves_the_manifest_remote(self) -> None:
|
||||||
|
bootstrap = load_bootstrap_module()
|
||||||
|
remote = "git@example.test:private/repository.git"
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
remote,
|
||||||
|
bootstrap.clone_remote(
|
||||||
|
remote,
|
||||||
|
transport=bootstrap.REGISTERED_TRANSPORT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_public_https_transport_fails_closed_for_other_hosts(self) -> None:
|
||||||
|
bootstrap = load_bootstrap_module()
|
||||||
|
|
||||||
|
unsafe_remotes = (
|
||||||
|
"git@example.test:add-ideas/govoplan-core.git",
|
||||||
|
"https://token@git.add-ideas.de/add-ideas/govoplan-core.git",
|
||||||
|
"https://git.add-ideas.de:443/add-ideas/govoplan-core.git",
|
||||||
|
"https://git.add-ideas.de/add-ideas/../govoplan-core.git",
|
||||||
|
"https://git.add-ideas.de/add-ideas/govoplan-core.git?ref=main",
|
||||||
|
"ssh://root@git.add-ideas.de/add-ideas/govoplan-core.git",
|
||||||
|
)
|
||||||
|
for remote in unsafe_remotes:
|
||||||
|
with self.subTest(remote=remote), self.assertRaises(ValueError):
|
||||||
|
bootstrap.clone_remote(
|
||||||
|
remote,
|
||||||
|
transport=bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_main_clones_missing_repositories_over_public_https(self) -> None:
|
||||||
|
bootstrap = load_bootstrap_module()
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-bootstrap-") as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
parent = root / "checkouts"
|
||||||
|
parent.mkdir()
|
||||||
|
root.joinpath("repositories.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"default_parent": str(parent),
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"name": "govoplan-core",
|
||||||
|
"path": "govoplan-core",
|
||||||
|
"remote": (
|
||||||
|
"git@git.add-ideas.de:"
|
||||||
|
"add-ideas/govoplan-core.git"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
patch.object(bootstrap, "ROOT", root),
|
||||||
|
patch.object(bootstrap.subprocess, "run") as runner,
|
||||||
|
):
|
||||||
|
status = bootstrap.main(
|
||||||
|
[
|
||||||
|
"--parent",
|
||||||
|
str(parent),
|
||||||
|
"--transport",
|
||||||
|
bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(0, status)
|
||||||
|
runner.assert_called_once_with(
|
||||||
|
[
|
||||||
|
"git",
|
||||||
|
"-c",
|
||||||
|
"credential.helper=",
|
||||||
|
"clone",
|
||||||
|
"--",
|
||||||
|
"https://git.add-ideas.de/add-ideas/govoplan-core.git",
|
||||||
|
str(parent / "govoplan-core"),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
env=runner.call_args.kwargs["env"],
|
||||||
|
)
|
||||||
|
environment = runner.call_args.kwargs["env"]
|
||||||
|
self.assertEqual("/bin/false", environment["GIT_ASKPASS"])
|
||||||
|
self.assertEqual("0", environment["GIT_TERMINAL_PROMPT"])
|
||||||
|
|
||||||
|
def test_main_validates_every_remote_before_cloning(self) -> None:
|
||||||
|
bootstrap = load_bootstrap_module()
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-bootstrap-") as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
parent = root / "checkouts"
|
||||||
|
parent.mkdir()
|
||||||
|
root.joinpath("repositories.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"default_parent": str(parent),
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"name": "govoplan-core",
|
||||||
|
"path": "govoplan-core",
|
||||||
|
"remote": (
|
||||||
|
"git@git.add-ideas.de:"
|
||||||
|
"add-ideas/govoplan-core.git"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "unsafe",
|
||||||
|
"path": "unsafe",
|
||||||
|
"remote": "git@example.test:private/unsafe.git",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
patch.object(bootstrap, "ROOT", root),
|
||||||
|
patch.object(bootstrap.subprocess, "run") as runner,
|
||||||
|
self.assertRaises(ValueError),
|
||||||
|
):
|
||||||
|
bootstrap.main(
|
||||||
|
[
|
||||||
|
"--parent",
|
||||||
|
str(parent),
|
||||||
|
"--transport",
|
||||||
|
bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
runner.assert_not_called()
|
||||||
|
|
||||||
|
def test_main_preserves_an_explicit_private_repository_transport(self) -> None:
|
||||||
|
bootstrap = load_bootstrap_module()
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-bootstrap-") as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
parent = root / "checkouts"
|
||||||
|
parent.mkdir()
|
||||||
|
root.joinpath("repositories.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"default_parent": str(parent),
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"name": "website",
|
||||||
|
"path": "website",
|
||||||
|
"remote": (
|
||||||
|
"git@git.add-ideas.de:"
|
||||||
|
"add-ideas/addideas-govoplan-website.git"
|
||||||
|
),
|
||||||
|
"bootstrap_transport": (
|
||||||
|
bootstrap.REGISTERED_TRANSPORT
|
||||||
|
),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
patch.object(bootstrap, "ROOT", root),
|
||||||
|
patch.object(bootstrap.subprocess, "run") as runner,
|
||||||
|
):
|
||||||
|
status = bootstrap.main(
|
||||||
|
[
|
||||||
|
"--parent",
|
||||||
|
str(parent),
|
||||||
|
"--transport",
|
||||||
|
bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(0, status)
|
||||||
|
command = runner.call_args.args[0]
|
||||||
|
self.assertEqual(
|
||||||
|
"git@git.add-ideas.de:add-ideas/addideas-govoplan-website.git",
|
||||||
|
command[-2],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_main_limits_bootstrap_to_selected_repositories(self) -> None:
|
||||||
|
bootstrap = load_bootstrap_module()
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-bootstrap-") as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
parent = root / "checkouts"
|
||||||
|
parent.mkdir()
|
||||||
|
root.joinpath("repositories.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"default_parent": str(parent),
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"path": name,
|
||||||
|
"remote": (
|
||||||
|
"git@git.add-ideas.de:add-ideas/"
|
||||||
|
f"{name}.git"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for name in ("govoplan-core", "govoplan-poll")
|
||||||
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
for repository_filter in (
|
||||||
|
["--repo", "govoplan-core"],
|
||||||
|
["--exclude-repo", "govoplan-poll"],
|
||||||
|
):
|
||||||
|
with (
|
||||||
|
self.subTest(repository_filter=repository_filter),
|
||||||
|
patch.object(bootstrap, "ROOT", root),
|
||||||
|
patch.object(bootstrap.subprocess, "run") as runner,
|
||||||
|
):
|
||||||
|
status = bootstrap.main(
|
||||||
|
[
|
||||||
|
"--parent",
|
||||||
|
str(parent),
|
||||||
|
"--transport",
|
||||||
|
bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||||
|
*repository_filter,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertEqual(0, status)
|
||||||
|
runner.assert_called_once()
|
||||||
|
self.assertEqual(
|
||||||
|
"https://git.add-ideas.de/add-ideas/govoplan-core.git",
|
||||||
|
runner.call_args.args[0][-2],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_main_rejects_unknown_or_conflicting_repository_filters(self) -> None:
|
||||||
|
bootstrap = load_bootstrap_module()
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-bootstrap-") as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
parent = root / "checkouts"
|
||||||
|
parent.mkdir()
|
||||||
|
root.joinpath("repositories.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"default_parent": str(parent),
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"name": "govoplan-core",
|
||||||
|
"path": "govoplan-core",
|
||||||
|
"remote": (
|
||||||
|
"git@git.add-ideas.de:"
|
||||||
|
"add-ideas/govoplan-core.git"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
patch.object(bootstrap, "ROOT", root),
|
||||||
|
patch.object(bootstrap.subprocess, "run") as runner,
|
||||||
|
):
|
||||||
|
with self.assertRaisesRegex(ValueError, "unknown registered"):
|
||||||
|
bootstrap.main(["--repo", "govoplan-missing"])
|
||||||
|
with self.assertRaisesRegex(ValueError, "selected and excluded"):
|
||||||
|
bootstrap.main(
|
||||||
|
[
|
||||||
|
"--repo",
|
||||||
|
"govoplan-core",
|
||||||
|
"--exclude-repo",
|
||||||
|
"govoplan-core",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
runner.assert_not_called()
|
||||||
|
|
||||||
|
def test_check_reports_missing_without_cloning(self) -> None:
|
||||||
|
bootstrap = load_bootstrap_module()
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-bootstrap-") as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
parent = root / "checkouts"
|
||||||
|
parent.mkdir()
|
||||||
|
root.joinpath("repositories.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"default_parent": str(parent),
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"name": "govoplan-core",
|
||||||
|
"path": "govoplan-core",
|
||||||
|
"remote": (
|
||||||
|
"git@git.add-ideas.de:"
|
||||||
|
"add-ideas/govoplan-core.git"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
patch.object(bootstrap, "ROOT", root),
|
||||||
|
patch.object(bootstrap.subprocess, "run") as runner,
|
||||||
|
):
|
||||||
|
status = bootstrap.main(
|
||||||
|
[
|
||||||
|
"--check",
|
||||||
|
"--parent",
|
||||||
|
str(parent),
|
||||||
|
"--transport",
|
||||||
|
bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(1, status)
|
||||||
|
runner.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
0
tools/checks/security-audit/run.sh
Normal file → Executable file
0
tools/checks/security-audit/run.sh
Normal file → Executable file
@@ -3,35 +3,136 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
REGISTERED_TRANSPORT = "registered"
|
||||||
|
PUBLIC_HTTPS_TRANSPORT = "public-https"
|
||||||
|
GITEA_SSH_PREFIX = "git@git.add-ideas.de:"
|
||||||
|
GITEA_HTTPS_PREFIX = "https://git.add-ideas.de/"
|
||||||
|
GITEA_REPOSITORY_PATH = re.compile(r"add-ideas/[a-z0-9][a-z0-9-]*[.]git")
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(description="Clone GovOPlaN repositories listed in repositories.json.")
|
parser = argparse.ArgumentParser(description="Clone GovOPlaN repositories listed in repositories.json.")
|
||||||
parser.add_argument("--check", action="store_true", help="Only report missing repositories.")
|
parser.add_argument("--check", action="store_true", help="Only report missing repositories.")
|
||||||
parser.add_argument("--parent", type=Path, help="Override checkout parent directory.")
|
parser.add_argument("--parent", type=Path, help="Override checkout parent directory.")
|
||||||
return parser.parse_args()
|
parser.add_argument(
|
||||||
|
"--repo",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
metavar="NAME",
|
||||||
|
help="Clone only this registered repository; repeat for more than one.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--exclude-repo",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
metavar="NAME",
|
||||||
|
help="Exclude this registered repository; repeat for more than one.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--transport",
|
||||||
|
choices=(REGISTERED_TRANSPORT, PUBLIC_HTTPS_TRANSPORT),
|
||||||
|
default=REGISTERED_TRANSPORT,
|
||||||
|
help=(
|
||||||
|
"Clone registered remotes unchanged, or convert public "
|
||||||
|
"git.add-ideas.de SSH remotes to anonymous HTTPS."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def clone_remote(remote: str, *, transport: str) -> str:
|
||||||
args = parse_args()
|
if transport == REGISTERED_TRANSPORT:
|
||||||
manifest = json.loads((ROOT / "repositories.json").read_text())
|
return remote
|
||||||
|
if transport != PUBLIC_HTTPS_TRANSPORT:
|
||||||
|
raise ValueError(f"unsupported repository transport: {transport}")
|
||||||
|
if remote.startswith(GITEA_SSH_PREFIX):
|
||||||
|
repository_path = remote.removeprefix(GITEA_SSH_PREFIX)
|
||||||
|
else:
|
||||||
|
parsed = urlsplit(remote)
|
||||||
|
if (
|
||||||
|
parsed.scheme not in {"https", "ssh"}
|
||||||
|
or parsed.hostname != "git.add-ideas.de"
|
||||||
|
or parsed.port is not None
|
||||||
|
or parsed.password is not None
|
||||||
|
or parsed.query
|
||||||
|
or parsed.fragment
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"public HTTPS bootstrap only accepts registered "
|
||||||
|
"git.add-ideas.de remotes"
|
||||||
|
)
|
||||||
|
if parsed.scheme == "https" and parsed.username is not None:
|
||||||
|
raise ValueError("public HTTPS bootstrap does not accept credentials")
|
||||||
|
if parsed.scheme == "ssh" and parsed.username != "git":
|
||||||
|
raise ValueError("registered Gitea SSH remotes must use the git account")
|
||||||
|
repository_path = parsed.path.removeprefix("/")
|
||||||
|
if GITEA_REPOSITORY_PATH.fullmatch(repository_path) is None:
|
||||||
|
raise ValueError("registered Gitea repository path is malformed")
|
||||||
|
return GITEA_HTTPS_PREFIX + repository_path
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
args = parse_args(argv)
|
||||||
|
manifest = json.loads((ROOT / "repositories.json").read_text(encoding="utf-8"))
|
||||||
parent = args.parent or Path(manifest["default_parent"])
|
parent = args.parent or Path(manifest["default_parent"])
|
||||||
missing: list[dict[str, str]] = []
|
missing: list[tuple[dict[str, str], Path, str]] = []
|
||||||
|
repositories = manifest["repositories"]
|
||||||
|
names = [entry["name"] for entry in repositories]
|
||||||
|
if len(names) != len(set(names)):
|
||||||
|
raise ValueError("repository registry contains duplicate names")
|
||||||
|
requested = set(args.repo)
|
||||||
|
excluded = set(args.exclude_repo)
|
||||||
|
unknown = (requested | excluded) - set(names)
|
||||||
|
if unknown:
|
||||||
|
raise ValueError(f"unknown registered repositories: {', '.join(sorted(unknown))}")
|
||||||
|
overlap = requested & excluded
|
||||||
|
if overlap:
|
||||||
|
raise ValueError(
|
||||||
|
f"repositories cannot be selected and excluded: {', '.join(sorted(overlap))}"
|
||||||
|
)
|
||||||
|
|
||||||
for entry in manifest["repositories"]:
|
for entry in repositories:
|
||||||
|
if (requested and entry["name"] not in requested) or entry["name"] in excluded:
|
||||||
|
continue
|
||||||
repo = parent / entry["path"]
|
repo = parent / entry["path"]
|
||||||
if repo.exists():
|
if repo.exists():
|
||||||
continue
|
continue
|
||||||
missing.append(entry)
|
transport = entry.get("bootstrap_transport", args.transport)
|
||||||
|
remote = clone_remote(entry["remote"], transport=transport)
|
||||||
|
missing.append((entry, repo, remote))
|
||||||
|
|
||||||
|
environment = os.environ.copy()
|
||||||
|
environment.update(
|
||||||
|
{
|
||||||
|
"GIT_ASKPASS": "/bin/false",
|
||||||
|
"GIT_TERMINAL_PROMPT": "0",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for entry, repo, remote in missing:
|
||||||
print(f"missing: {entry['name']} -> {repo}")
|
print(f"missing: {entry['name']} -> {repo}")
|
||||||
if not args.check:
|
if not args.check:
|
||||||
subprocess.run(["git", "clone", entry["remote"], str(repo)], check=True)
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"git",
|
||||||
|
"-c",
|
||||||
|
"credential.helper=",
|
||||||
|
"clone",
|
||||||
|
"--",
|
||||||
|
remote,
|
||||||
|
str(repo),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
env=environment,
|
||||||
|
)
|
||||||
|
|
||||||
if args.check:
|
if args.check:
|
||||||
return 1 if missing else 0
|
return 1 if missing else 0
|
||||||
|
|||||||
Reference in New Issue
Block a user