diff --git a/.github/workflows/no-secrets.yml b/.github/workflows/no-secrets.yml new file mode 100644 index 0000000..cc6e028 --- /dev/null +++ b/.github/workflows/no-secrets.yml @@ -0,0 +1,27 @@ +name: Tracked Secret Hygiene + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: secret-hygiene-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + secret-scan: + name: Scan tracked text files + runs-on: ubuntu-latest + steps: + - name: Check out source + uses: actions/checkout@v4 + + - name: Scan for secret-like values + shell: bash + run: php scripts/no-secrets-check.php diff --git a/docs/ENGINE_IMPLEMENTATION_AUDIT.md b/docs/ENGINE_IMPLEMENTATION_AUDIT.md index 50b5fcd..83983e9 100644 --- a/docs/ENGINE_IMPLEMENTATION_AUDIT.md +++ b/docs/ENGINE_IMPLEMENTATION_AUDIT.md @@ -51,6 +51,12 @@ The 31-case policy result currently linked from the README is historical evidenc Legal provenance is also unresolved on current `main`: the root `LICENSE` path is absent, and GitHub reports no repository license. The README's proprietary-license statement remains the only checked-in guidance. This audit does not invent an MIT or other license; PR #6 proposes a license change and should be resolved by its owner before any legal claim is published. Preserve existing attribution and history until that choice is merged. +## Tracked configuration and secret scan + +The repository currently has no tracked `.env` or `.env.*` files; `.gitignore` excludes runtime environment files while allowing future redacted `.env.example` templates. The new `scripts/no-secrets-check.php` scans Git-tracked text files and reports only path, line, and rule identifiers—never matched values. It rejects private-key markers, common provider/GitHub/AWS token formats, and credential-like assignments. + +The sole allowlisted match is the deterministic evaluator fixture `interaction-policy-eval-secret-2026` in `scripts/authority-policy-eval.php`; it is not a runtime credential and is matched by exact path and literal. The [tracked secret hygiene workflow](../.github/workflows/no-secrets.yml) runs the check on pull requests and pushes to `main`. Branch-protection required status is not asserted here and must be verified separately before relying on it as a merge gate. + ## Recommended next work Implement each no-op method only with its owning contract's intended semantics and a focused test. Until then, keep the methods visible as compatibility scaffolds and keep portfolio language at the deterministic baseline level recorded in [AI_ENGINEERING.md](AI_ENGINEERING.md). diff --git a/scripts/no-secrets-check.php b/scripts/no-secrets-check.php new file mode 100644 index 0000000..4f56734 --- /dev/null +++ b/scripts/no-secrets-check.php @@ -0,0 +1,76 @@ + $path !== '')); +$rules = [ + 'private-key' => '/-----BEGIN [^-]+ PRIVATE KEY-----/', + 'known-token' => '/(? '/(? '/\bAKIA[0-9A-Z]{16}\b/', + 'credential-assignment' => "/\b(?:api[_-]?key|secret|password|token)\b\s*[:=]\s*[\"']?([A-Za-z0-9_\/.=+\-]{12,})/i", +]; +$allowlistedFixtures = [ + 'scripts/authority-policy-eval.php' => 'interaction-policy-eval-secret-2026', +]; + +$findings = []; +$allowlistedMatches = 0; +$textFileCount = 0; + +foreach ($trackedFiles as $relativePath) { + $absolutePath = $root . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relativePath); + if (!is_file($absolutePath)) { + continue; + } + + $contents = file_get_contents($absolutePath); + if (!is_string($contents) || str_contains($contents, "\0")) { + continue; + } + $textFileCount++; + + foreach ($rules as $rule => $pattern) { + $matches = []; + if (preg_match_all($pattern, $contents, $matches, PREG_OFFSET_CAPTURE) === false || $matches[0] === []) { + continue; + } + + foreach ($matches[0] as $match) { + $value = (string) $match[0]; + $offset = (int) $match[1]; + $line = 1 + substr_count(substr($contents, 0, $offset), "\n"); + $prefix = substr($contents, 0, $offset + strlen($value)); + $lines = explode("\n", $prefix); + $lineText = (string) ($lines[array_key_last($lines)] ?? ''); + $isAllowlistedFixture = $rule === 'credential-assignment' + && isset($allowlistedFixtures[$relativePath]) + && str_contains($lineText, $allowlistedFixtures[$relativePath]); + + if ($isAllowlistedFixture) { + $allowlistedMatches++; + continue; + } + + $findings[$relativePath . ':' . $line . ':' . $rule] = true; + } + } +} + +if ($findings !== []) { + ksort($findings); + fwrite(STDERR, "Secret-like values detected (values redacted):\n"); + foreach (array_keys($findings) as $finding) { + fwrite(STDERR, " - {$finding}\n"); + } + exit(1); +} + +echo "No secret-like values found in {$textFileCount} tracked text files; allowlisted deterministic fixture matches: {$allowlistedMatches}.\n";