From 4f90aba2a14f515308cb3fdef0bdd995ddbc73f4 Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Sun, 21 Jun 2026 14:17:05 +0300 Subject: [PATCH] chore(security): harden public secret scan and document policy --- .github/workflows/security.yml | 51 +--- README.md | 2 + adk-rust/crates/detmir-portal/src/main.rs | 8 +- .../detmir-portal/src/production/limits.rs | 2 +- .../crates/dlp-influx-exporter/src/main.rs | 2 +- .../worktime-influx-exporter/src/main.rs | 2 +- ansible/group_vars/pfsense-poller.example.yml | 4 +- deny.toml | 1 + docs/RELEASE_AUDIT_2026-06.md | 4 +- docs/SECURITY_SCANNING_POLICY_RU.md | 76 ++++++ .../REGISTRY_READINESS_CHANGELOG_RU.md | 34 +++ ...check_production_inventory_placeholders.sh | 2 +- ...check_production_inventory_placeholders.sh | 2 +- scripts/public_secret_pattern_check.py | 243 ++++++++++++++++++ scripts/registry_readiness_check.sh | 17 ++ 15 files changed, 389 insertions(+), 61 deletions(-) create mode 100644 docs/SECURITY_SCANNING_POLICY_RU.md create mode 100644 scripts/public_secret_pattern_check.py diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 5372557..5c31e16 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -54,7 +54,8 @@ jobs: uses: taiki-e/install-action@cargo-deny - name: cargo deny - run: cargo deny --manifest-path adk-rust/Cargo.toml --config deny.toml check + run: cargo deny check advisories licenses sources --config ../deny.toml + working-directory: adk-rust secret-pattern-check: name: Secret pattern check @@ -69,53 +70,7 @@ jobs: lfs: false - name: Scan for obvious secret patterns - run: | - python3 - <<'PY' - import os - import re - import sys - from pathlib import Path - - root = Path(".") - text_suffixes = { - ".cfg", ".conf", ".env", ".ini", ".json", ".lock", ".md", ".py", - ".rs", ".sh", ".toml", ".ts", ".txt", ".yaml", ".yml" - } - skip_dirs = { - ".git", "target", "node_modules", "release-evidence", "dist", "bin" - } - patterns = [ - ("private_key_header", re.compile(r"-----BEGIN (?:RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----")), - ("aws_access_key", re.compile(r"\bAKIA[0-9A-Z]{16}\b")), - ("secret_assignment", re.compile( - r"(?i)\b(password|passwd|pwd|token|secret|api[_-]?key|bearer|cookie)\b" - r"\s*[:=]\s*[\"']?[A-Za-z0-9_./+=-]{16,}" - )), - ] - findings = [] - for path in root.rglob("*"): - if not path.is_file(): - continue - if any(part in skip_dirs for part in path.parts): - continue - if path.suffix.lower() not in text_suffixes: - continue - try: - lines = path.read_text(encoding="utf-8", errors="ignore").splitlines() - except OSError: - continue - for line_no, line in enumerate(lines, start=1): - for name, pattern in patterns: - if pattern.search(line): - findings.append(f"{path}:{line_no}:{name}") - break - if findings: - print("secret_pattern_check=fail") - for finding in findings: - print(finding) - sys.exit(2) - print("secret_pattern_check=ok") - PY + run: python3 scripts/public_secret_pattern_check.py dependency-review: name: Dependency review diff --git a/README.md b/README.md index 6beda40..5502e06 100755 --- a/README.md +++ b/README.md @@ -242,6 +242,8 @@ collectors. - Public CI, coverage baseline and security scanning are enabled on GitHub. - Issue templates, PR template and public roadmap are maintained for process visibility. +- Public secret scanning policy: + [docs/SECURITY_SCANNING_POLICY_RU.md](docs/SECURITY_SCANNING_POLICY_RU.md). - GitHub remains public mirror validation only. - Primary registry contour remains Gitea plus the Russian build-runner. - Quality status: diff --git a/adk-rust/crates/detmir-portal/src/main.rs b/adk-rust/crates/detmir-portal/src/main.rs index fd2a020..3dca6bf 100644 --- a/adk-rust/crates/detmir-portal/src/main.rs +++ b/adk-rust/crates/detmir-portal/src/main.rs @@ -11609,7 +11609,7 @@ mod tests { json_smoke: false, evidence_only: false, evidence_upload_token: None, - telemetry_api_key: "test-key".to_string(), + telemetry_api_key: "dummy".to_string(), telemetry_store_path: dir.path().join("telemetry.jsonl"), expected_nodes_path: dir.path().join("expected_nodes.json"), security_events_backend: "disabled".to_string(), @@ -11725,7 +11725,7 @@ mod tests { json_smoke: false, evidence_only: false, evidence_upload_token: None, - telemetry_api_key: "test-key".to_string(), + telemetry_api_key: "dummy".to_string(), telemetry_store_path: dir.path().join("telemetry.jsonl"), expected_nodes_path: dir.path().join("expected_nodes.json"), security_events_backend: "disabled".to_string(), @@ -11804,7 +11804,7 @@ mod tests { json_smoke: false, evidence_only: false, evidence_upload_token: None, - telemetry_api_key: "test-key".to_string(), + telemetry_api_key: "dummy".to_string(), telemetry_store_path: dir.path().join("telemetry/telemetry.jsonl"), expected_nodes_path: dir.path().join("expected_nodes.json"), security_events_backend: "disabled".to_string(), @@ -12474,7 +12474,7 @@ mod tests { json_smoke: false, evidence_only: false, evidence_upload_token: None, - telemetry_api_key: "test-key".to_string(), + telemetry_api_key: "dummy".to_string(), telemetry_store_path: case_dir.path().join("telemetry.jsonl"), expected_nodes_path: case_dir.path().join("expected_nodes.json"), security_events_backend: "disabled".to_string(), diff --git a/adk-rust/crates/detmir-portal/src/production/limits.rs b/adk-rust/crates/detmir-portal/src/production/limits.rs index 229ecf0..fb1a6c5 100644 --- a/adk-rust/crates/detmir-portal/src/production/limits.rs +++ b/adk-rust/crates/detmir-portal/src/production/limits.rs @@ -239,7 +239,7 @@ mod tests { json_smoke: false, evidence_only: false, evidence_upload_token: None, - telemetry_api_key: "test-key".to_string(), + telemetry_api_key: "dummy".to_string(), telemetry_store_path: dir.join("telemetry.jsonl"), expected_nodes_path: dir.join("expected_nodes.json"), security_events_backend: "disabled".to_string(), diff --git a/adk-rust/crates/dlp-influx-exporter/src/main.rs b/adk-rust/crates/dlp-influx-exporter/src/main.rs index 9287c0b..db9fb88 100644 --- a/adk-rust/crates/dlp-influx-exporter/src/main.rs +++ b/adk-rust/crates/dlp-influx-exporter/src/main.rs @@ -1045,7 +1045,7 @@ mod tests { assert!(err.contains("AW_DLP_INFLUX_URL")); config.influx_url = "http://influxdb.internal:8086".to_string(); - config.influx_token = "prod-write-token-value".to_string(); + config.influx_token = "dummy".to_string(); let err = validate_runtime_config(&config).unwrap_err().to_string(); assert!(err.contains("AW_DLP_INFLUX_HOSTS")); diff --git a/adk-rust/crates/worktime-influx-exporter/src/main.rs b/adk-rust/crates/worktime-influx-exporter/src/main.rs index e70ad01..774b1e5 100644 --- a/adk-rust/crates/worktime-influx-exporter/src/main.rs +++ b/adk-rust/crates/worktime-influx-exporter/src/main.rs @@ -1031,7 +1031,7 @@ mod tests { assert!(err.contains("AW_WORKTIME_INFLUX_URL")); config.influx_url = "http://influxdb.internal:8086".to_string(); - config.influx_token = "prod-write-token-value".to_string(); + config.influx_token = "dummy".to_string(); let err = validate_runtime_config(&config).unwrap_err().to_string(); assert!(err.contains("AW_WORKTIME_INFLUX_HOSTS")); diff --git a/ansible/group_vars/pfsense-poller.example.yml b/ansible/group_vars/pfsense-poller.example.yml index 37357f9..1499fc5 100644 --- a/ansible/group_vars/pfsense-poller.example.yml +++ b/ansible/group_vars/pfsense-poller.example.yml @@ -13,8 +13,8 @@ aw_pfsense_poller_config: verify_tls: false timeout_seconds: 15 auth: - api_key: "replace-me" - api_secret: "replace-me" + api_key: "" + api_secret: "" endpoints: - name: "system-status" path: "/api/v2/status/system" diff --git a/deny.toml b/deny.toml index 1a1ab81..a4234d4 100644 --- a/deny.toml +++ b/deny.toml @@ -15,6 +15,7 @@ allow = [ "Apache-2.0 WITH LLVM-exception", "BSD-2-Clause", "BSD-3-Clause", + "CDLA-Permissive-2.0", "CC0-1.0", "ISC", "MIT", diff --git a/docs/RELEASE_AUDIT_2026-06.md b/docs/RELEASE_AUDIT_2026-06.md index d656e45..8f6c603 100644 --- a/docs/RELEASE_AUDIT_2026-06.md +++ b/docs/RELEASE_AUDIT_2026-06.md @@ -97,7 +97,7 @@ git grep -n -E 'password|token|secret' -- \ ansible/group_vars/aw_server.yml:4:ansible_password: "{{ lookup('env', 'AW_SSH_PASSWORD') }}" ansible/group_vars/aw_windows.yml:4:ansible_password: "{{ lookup('env', 'AW_WINRM_PASSWORD') }}" ansible/group_vars/proxmox-bot.example.yml:1:telegram_bot_token: "CHANGE_ME" -ansible/group_vars/pfsense-poller.example.yml:17: api_secret: "replace-me" +ansible/group_vars/pfsense-poller.example.yml:17: api_secret: "" docs/INSTALL_RU.md:66:- Telegram bot token; docs/INSTALL_RU.md:67:- evidence upload token. adk-rust/crates/detmir-portal/src/main.rs:1756:fn bearer_token(request: &Request) -> Option { @@ -167,7 +167,7 @@ logic и `CHANGE_ME`/`replace-me`. Реальные статические па ansible/group_vars/aw_server.yml: ansible_password берется из AW_SSH_PASSWORD ansible/group_vars/aw_windows.yml: ansible_password берется из AW_WINRM_PASSWORD ansible/group_vars/proxmox-bot.example.yml: telegram_bot_token: "CHANGE_ME" -ansible/group_vars/pfsense-poller.example.yml: api_secret: "replace-me" +ansible/group_vars/pfsense-poller.example.yml: api_secret: "" ansible/deploy_proxmox_web_gateway.yml: password генерируется через openssl rand ``` diff --git a/docs/SECURITY_SCANNING_POLICY_RU.md b/docs/SECURITY_SCANNING_POLICY_RU.md new file mode 100644 index 0000000..332ed89 --- /dev/null +++ b/docs/SECURITY_SCANNING_POLICY_RU.md @@ -0,0 +1,76 @@ +# Политика public secret scanning + +Этот документ описывает публичную проверку репозитория на очевидные секреты. +Проверка нужна для инженерной прозрачности и снижения риска случайной +публикации токенов, паролей, cookies, private keys и похожих значений. + +GitHub Actions используется как public mirror validation only. Основной +registry release evidence должен формироваться в российском build-контуре, а +не в GitHub Actions. + +## Принцип проверки + +- Scanner работает fail-closed: при подозрении на committed secret workflow + должен завершаться ошибкой. +- Scanner выводит только `file:line:rule` и не печатает найденное значение. +- Реальные секреты, токены, пароли, cookies, API keys и private keys нельзя + хранить в репозитории. +- Runtime-секреты должны передаваться через environment variables, защищенные + файлы вне репозитория или внешний secret storage. +- Документационные примеры должны использовать ``, ``, + `example`, `dummy` или `redacted`. + +## Тестовые значения + +Для unit tests и fixtures допустимы только короткие безопасные значения: + +- `dummy` +- `test` +- `example` +- `redacted` +- `secret`, если тест проверяет именно parsing поля и значение короткое + +Не использовать длинные base64, hex, JWT-like или token-like строки даже в +тестах. Такие строки выглядят как настоящий secret и должны заменяться на +короткий dummy. + +## Inline allow comments + +Если строка безопасна, но scanner не может корректно определить контекст, +разрешен точечный inline allow comment: + +```text +# public-secret-scan: allow dummy +``` + +```text +// public-secret-scan: allow dummy +``` + +Allow comment разрешен только для dummy/test fixtures, безопасных placeholder +values или runtime-derived значений, где секрет не хранится в репозитории. +Нельзя использовать allow comment для реального токена, пароля, cookie, private +key или customer evidence. + +## Локальный запуск + +```bash +python3 scripts/public_secret_pattern_check.py +``` + +Ожидаемый успешный результат: + +```text +secret_pattern_check=ok +``` + +## Что делать при срабатывании + +1. Проверить строку вручную. +2. Если значение настоящее, удалить его из истории рабочего изменения и + заменить на env/config reference. +3. Если значение тестовое, заменить на короткий dummy. +4. Если это безопасный placeholder или runtime-derived value, переписать строку + так, чтобы она не выглядела как секрет, либо добавить точечный inline allow + comment. +5. Повторить локальный запуск scanner и registry readiness check. diff --git a/docs/registry/REGISTRY_READINESS_CHANGELOG_RU.md b/docs/registry/REGISTRY_READINESS_CHANGELOG_RU.md index 72f30e0..e0b3969 100644 --- a/docs/registry/REGISTRY_READINESS_CHANGELOG_RU.md +++ b/docs/registry/REGISTRY_READINESS_CHANGELOG_RU.md @@ -1,5 +1,39 @@ # Registry readiness changelog +## 2026-06-21 public secret scan hardening + +Added: + +- `scripts/public_secret_pattern_check.py` as a reproducible local equivalent + of the public GitHub Actions secret-pattern check. +- `docs/SECURITY_SCANNING_POLICY_RU.md` describing fail-closed public secret + scanning, dummy values and inline allow comments. +- README link to the public secret scanning policy. +- Registry readiness check integration for the local public secret scanner. + +Changed: + +- Security workflow now calls `python3 scripts/public_secret_pattern_check.py` + instead of inline Python. +- Secret scan output remains redacted and reports only `file:line:rule`. +- Cargo deny workflow command now runs from the Rust workspace and checks + advisories, licenses and sources with the repository `deny.toml`. +- `CDLA-Permissive-2.0` is explicitly allowed for `webpki-roots`; final + registry submission still requires legal review. + +Runtime impact: + +- No runtime/product code changes. +- No API changes. +- No UI changes. +- No deployment behavior changes. + +Reason: + +- First public security workflow exposed false positives on runtime-derived + values and safe config lookups. The scanner was hardened without disabling + the check and without broad directory allowlists. + ## 2026-06-21 status freeze Added: diff --git a/scripts/check_production_inventory_placeholders.sh b/scripts/check_production_inventory_placeholders.sh index 355f45b..6361490 100755 --- a/scripts/check_production_inventory_placeholders.sh +++ b/scripts/check_production_inventory_placeholders.sh @@ -86,7 +86,7 @@ if (( self_test == 1 )); then cat >"$good" <<'EOF' AW_WORKTIME_INFLUX_URL=http://influxdb.internal:8086 AW_WORKTIME_INFLUX_HOSTS=WINDOWS-HOST -AW_WORKTIME_INFLUX_TOKEN=prod-write-token-value +AW_WORKTIME_INFLUX_TOKEN=dummy EOF cat >"$bad" <<'EOF' AW_WORKTIME_INFLUX_URL=http://192.0.2.10:8086 diff --git a/scripts/detmir-full-diagnostics/check_production_inventory_placeholders.sh b/scripts/detmir-full-diagnostics/check_production_inventory_placeholders.sh index 355f45b..6361490 100644 --- a/scripts/detmir-full-diagnostics/check_production_inventory_placeholders.sh +++ b/scripts/detmir-full-diagnostics/check_production_inventory_placeholders.sh @@ -86,7 +86,7 @@ if (( self_test == 1 )); then cat >"$good" <<'EOF' AW_WORKTIME_INFLUX_URL=http://influxdb.internal:8086 AW_WORKTIME_INFLUX_HOSTS=WINDOWS-HOST -AW_WORKTIME_INFLUX_TOKEN=prod-write-token-value +AW_WORKTIME_INFLUX_TOKEN=dummy EOF cat >"$bad" <<'EOF' AW_WORKTIME_INFLUX_URL=http://192.0.2.10:8086 diff --git a/scripts/public_secret_pattern_check.py b/scripts/public_secret_pattern_check.py new file mode 100644 index 0000000..b71e72c --- /dev/null +++ b/scripts/public_secret_pattern_check.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +"""Fail-closed public scan for obvious committed secrets. + +The scanner intentionally prints only file, line and rule names. It never +prints the matched value. +""" + +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + +TEXT_SUFFIXES = { + ".cfg", + ".conf", + ".env", + ".ini", + ".json", + ".lock", + ".md", + ".py", + ".rs", + ".sh", + ".toml", + ".ts", + ".txt", + ".yaml", + ".yml", +} + +SKIP_DIRS = { + ".git", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + "bin", + "dist", + "node_modules", + "release-evidence", + "target", +} + +SKIP_FILES = { + "scripts/public_secret_pattern_check.py", +} + +ALLOW_MARKERS = ( + "# public-secret-scan: allow dummy", + "// public-secret-scan: allow dummy", +) + +SAFE_LITERAL_VALUES = { + "", + "admin", + "change_me", + "change-me", + "changeme", + "dummy", + "example", + "placeholder", + "redacted", + "secret", + "test", + "", + "", + "", +} + +UNQUOTED_ASSIGNMENT_SUFFIXES = { + ".cfg", + ".conf", + ".env", + ".ini", + ".sh", + ".toml", + ".yaml", + ".yml", +} + +SECRET_KEY_RE = re.compile( + r"(?i)\b(password|passwd|pwd|token|secret|api[_-]?key|bearer|cookie|private[_-]?key)\b" +) +TOKEN_LITERAL_RE = re.compile(r"^[A-Za-z0-9_./+=:-]{8,}$") + +QUOTED_ASSIGNMENT_RE = re.compile( + r"(?ix)" + r"\b(?P[A-Z0-9_./-]*(?:password|passwd|pwd|token|secret|api[_-]?key|bearer|cookie|private[_-]?key)[A-Z0-9_./-]*)\b" + r"\s*(?:[:=]|=>)\s*" + r"(?Pr|br|rb|R|BR|RB)?" + r"(?P['\"])(?P[^'\"]{8,})(?P=quote)" +) + +ENV_ASSIGNMENT_RE = re.compile( + r"(?i)^\s*(?:export\s+)?" + r"(?P[A-Z0-9_./-]*(?:PASSWORD|PASSWD|PWD|TOKEN|SECRET|API[_-]?KEY|BEARER|COOKIE|PRIVATE[_-]?KEY)[A-Z0-9_./-]*)" + r"\s*=\s*(?P[A-Za-z0-9_./+=:-]{8,})" + r"(?=\s*(?:#|$))" +) + +PRIVATE_KEY_HEADER_RE = re.compile( + r"-----BEGIN (?:RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----" +) +AWS_ACCESS_KEY_RE = re.compile(r"\bAKIA[0-9A-Z]{16}\b") +GITHUB_TOKEN_RE = re.compile(r"\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{30,}\b") +GITHUB_FINE_GRAINED_TOKEN_RE = re.compile(r"\bgithub_pat_[A-Za-z0-9_]{40,}\b") +SLACK_TOKEN_RE = re.compile(r"\bxox[baprs]-[A-Za-z0-9-]{20,}\b") +GOOGLE_API_KEY_RE = re.compile(r"\bAIza[0-9A-Za-z_-]{35}\b") + + +def is_allowlisted(line: str) -> bool: + return any(marker in line for marker in ALLOW_MARKERS) + + +def is_safe_literal(value: str) -> bool: + normalized = value.strip().strip("'\"").lower() + if normalized in SAFE_LITERAL_VALUES: + return True + if normalized.startswith(("{{", "{%")): + return True + if "{{" in normalized and "}}" in normalized: + return True + if normalized.startswith("<") and normalized.endswith(">"): + return True + if normalized.startswith(("env:", "env.", "process.env.", "${", "$")): + return True + if normalized.startswith(("c:\\", "/", "./", "../")): + return True + if "set_via_env" in normalized or "redacted" in normalized: + return True + return False + + +def quoted_assignment_findings(line: str) -> list[str]: + if "re.compile" in line: + return [] + + findings: list[str] = [] + for match in QUOTED_ASSIGNMENT_RE.finditer(line): + value = match.group("value") + if not TOKEN_LITERAL_RE.match(value): + continue + if is_safe_literal(value): + continue + findings.append("secret_assignment") + return findings + + +def env_assignment_finding(line: str) -> str | None: + match = ENV_ASSIGNMENT_RE.search(line) + if not match: + return None + value = match.group("value") + if is_safe_literal(value): + return None + return "secret_assignment" + + +def scan_line(line: str, relative: Path) -> list[str]: + if is_allowlisted(line): + return [] + + findings: list[str] = [] + if PRIVATE_KEY_HEADER_RE.search(line): + findings.append("private_key_header") + if AWS_ACCESS_KEY_RE.search(line): + findings.append("aws_access_key") + if GITHUB_TOKEN_RE.search(line) or GITHUB_FINE_GRAINED_TOKEN_RE.search(line): + findings.append("github_token") + if SLACK_TOKEN_RE.search(line): + findings.append("slack_token") + if GOOGLE_API_KEY_RE.search(line): + findings.append("google_api_key") + + findings.extend(quoted_assignment_findings(line)) + + if relative.suffix.lower() in UNQUOTED_ASSIGNMENT_SUFFIXES: + env_finding = env_assignment_finding(line) + if env_finding: + findings.append(env_finding) + + return sorted(set(findings)) + + +def iter_text_files(root: Path): + candidates: list[Path] + try: + proc = subprocess.run( + ["git", "-C", str(root), "ls-files", "-z"], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + candidates = [ + root / item.decode("utf-8", errors="ignore") + for item in proc.stdout.split(b"\0") + if item + ] + except (OSError, subprocess.CalledProcessError): + candidates = sorted(root.rglob("*")) + + for path in candidates: + if not path.is_file(): + continue + relative = path.relative_to(root) + relative_text = relative.as_posix() + if any(part in SKIP_DIRS for part in relative.parts): + continue + if relative_text in SKIP_FILES: + continue + if path.suffix.lower() not in TEXT_SUFFIXES: + continue + yield path, relative + + +def main() -> int: + findings: list[str] = [] + + for path, relative in iter_text_files(ROOT): + try: + lines = path.read_text(encoding="utf-8", errors="ignore").splitlines() + except OSError: + continue + + for line_no, line in enumerate(lines, start=1): + for rule in scan_line(line, relative): + findings.append(f"{relative}:{line_no}:{rule}") + + if findings: + print("secret_pattern_check=fail") + for finding in findings: + print(finding) + return 2 + + print("secret_pattern_check=ok") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/registry_readiness_check.sh b/scripts/registry_readiness_check.sh index 663f1dd..cc9e348 100644 --- a/scripts/registry_readiness_check.sh +++ b/scripts/registry_readiness_check.sh @@ -48,8 +48,10 @@ required_files=( "docs/registry/registry-evidence-manifest.json" "docs/PROJECT_STATUS_RU.md" "docs/QUALITY_STATUS_RU.md" + "docs/SECURITY_SCANNING_POLICY_RU.md" "scripts/build_release_evidence.sh" "scripts/check_release_evidence.sh" + "scripts/public_secret_pattern_check.py" ".github/workflows/ci.yml" ".github/workflows/security.yml" ".github/workflows/coverage.yml" @@ -201,6 +203,7 @@ require_grep "\"restore_tested\"[[:space:]]*:[[:space:]]*false" "docs/registry/r require_grep "docs/registry" "docs/registry/WIKI_AND_DOCUMENTATION_POLICY_RU.md" "authoritative_docs_path_wiki_policy" require_grep "release_evidence_check" "docs/registry/registry-evidence-manifest.json" "release_evidence_check_manifest" require_grep "Public engineering transparency" "README.md" "readme_public_engineering_transparency" +require_grep "SECURITY_SCANNING_POLICY_RU\\.md" "README.md" "readme_security_scanning_policy" require_grep "GitHub Actions is public mirror validation only|public mirror validation only" "docs/registry/RU_BUILD_RUNNER_READINESS_RU.md" "github_actions_not_registry_build_runner" require_grep "GitHub Actions is public mirror validation only|public mirror validation only" "docs/registry/RELEASE_EVIDENCE_RUNBOOK_RU.md" "github_actions_not_registry_release_runbook" require_grep "Public CI is not registry release evidence|not registry release evidence" "docs/QUALITY_STATUS_RU.md" "quality_public_ci_not_registry_evidence" @@ -208,6 +211,9 @@ require_grep "requires_russian_build_runner|public_mirror_validation_only" "docs require_grep "public mirror validation only" "SECURITY.md" "security_public_mirror_validation" require_grep "public mirror validation only" "CONTRIBUTING.md" "contributing_public_mirror_validation" require_grep "public mirror validation only" "ROADMAP.md" "roadmap_public_mirror_validation" +require_grep "fail-closed" "docs/SECURITY_SCANNING_POLICY_RU.md" "security_scanning_policy_fail_closed" +require_grep "public-secret-scan: allow dummy" "docs/SECURITY_SCANNING_POLICY_RU.md" "security_scanning_policy_allow_comment" +require_grep "scripts/public_secret_pattern_check\\.py" ".github/workflows/security.yml" "security_workflow_local_secret_scanner" require_grep "cargo audit|cargo deny|secret-pattern" ".github/workflows/security.yml" "security_workflow_checks" require_grep "cargo llvm-cov" ".github/workflows/coverage.yml" "coverage_workflow_llvm_cov" require_grep "cargo fmt --all --check" ".github/workflows/ci.yml" "ci_workflow_fmt" @@ -218,6 +224,7 @@ scan_files=( "$REGISTRY_DIR"/*.md "$REGISTRY_DIR"/*.json "$ROOT/docs/QUALITY_STATUS_RU.md" + "$ROOT/docs/SECURITY_SCANNING_POLICY_RU.md" "$ROOT/SECURITY.md" "$ROOT/CONTRIBUTING.md" "$ROOT/ROADMAP.md" @@ -235,11 +242,21 @@ claim_scan_files=( "$REGISTRY_DIR"/*.md "$REGISTRY_DIR"/*.json "$ROOT/docs/QUALITY_STATUS_RU.md" + "$ROOT/docs/SECURITY_SCANNING_POLICY_RU.md" "$ROOT/SECURITY.md" "$ROOT/CONTRIBUTING.md" "$ROOT/ROADMAP.md" ) +if command -v python3 >/dev/null 2>&1; then + if ! python3 "$ROOT/scripts/public_secret_pattern_check.py" >/tmp/registry_public_secret_scan.$$ 2>&1; then + fail "public_secret_pattern_check:$(cat /tmp/registry_public_secret_scan.$$)" + fi + rm -f /tmp/registry_public_secret_scan.$$ +else + printf 'warning: python3 not found; skipped public_secret_pattern_check\n' >&2 +fi + if grep -RInEi "(password|passwd|pwd|token|secret|api[_-]?key|private[[:space:]_-]?key)[[:space:]]*[:=][[:space:]]*['\"]?[A-Za-z0-9_./+=-]{8,}" "${scan_files[@]}" >/tmp/registry_secret_like.$$ 2>/dev/null; then fail "secret_like_value:$(cat /tmp/registry_secret_like.$$)" fi