Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/no-secrets.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions docs/ENGINE_IMPLEMENTATION_AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
76 changes: 76 additions & 0 deletions scripts/no-secrets-check.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

$root = dirname(__DIR__);
$trackedOutput = shell_exec('git -C ' . escapeshellarg($root) . ' ls-files -z');
if (!is_string($trackedOutput)) {
fwrite(STDERR, "Unable to enumerate tracked files.\n");
exit(2);
}

$trackedFiles = array_values(array_filter(explode("\0", $trackedOutput), static fn(string $path): bool => $path !== ''));
$rules = [
'private-key' => '/-----BEGIN [^-]+ PRIVATE KEY-----/',
'known-token' => '/(?<![A-Za-z0-9])(?:sk|pk|rk)-[A-Za-z0-9]{16,}(?![A-Za-z0-9])/',
'github-token' => '/(?<![A-Za-z0-9])(?:ghp|github_pat)_[A-Za-z0-9_]{20,}(?![A-Za-z0-9])/',
'aws-access-key' => '/\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";
Loading