| 1 | #!/usr/bin/env bash |
| 2 | # Scan durable SOW-related artifacts for sensitive values. |
| 3 | # Usage: .agents/sow/scan-sensitive.sh FILE... |
| 4 | |
| 5 | set -uo pipefail |
| 6 | |
| 7 | if [ "$#" -eq 0 ]; then |
| 8 | echo "usage: $0 FILE..." >&2 |
| 9 | exit 2 |
| 10 | fi |
| 11 | |
| 12 | failures=0 |
| 13 | |
| 14 | if ! command -v perl >/dev/null 2>&1; then |
| 15 | echo "perl is required for sensitive-data scanning" >&2 |
| 16 | exit 2 |
| 17 | fi |
| 18 | |
| 19 | scan_sensitive_file() { |
| 20 | local file="$1" |
| 21 | perl -ne ' |
| 22 | chomp; |
| 23 | my $line = $_; |
| 24 | my @hits; |
| 25 | next if $ARGV =~ m{(^|/)\.agents/sow/scan-sensitive\.sh$} |
| 26 | && $line =~ /^\s*(?:push \@hits, "[^"]+"\s+(?:if|unless)\s+\$line =~|if \(\$line =~)/; |
| 27 | |
| 28 | sub is_public_customer_ip { |
| 29 | my ($ip) = @_; |
| 30 | my @o = split(/\./, $ip); |
| 31 | return 0 unless @o == 4; |
| 32 | return 0 if grep { $_ !~ /^\d+$/ || $_ < 0 || $_ > 255 } @o; |
| 33 | return 0 if $o[0] == 10; |
| 34 | return 0 if $o[0] == 172 && $o[1] >= 16 && $o[1] <= 31; |
| 35 | return 0 if $o[0] == 192 && $o[1] == 168; |
| 36 | return 0 if $o[0] == 127; |
| 37 | return 0 if $o[0] == 169 && $o[1] == 254; |
| 38 | return 0 if $o[0] == 100 && $o[1] >= 64 && $o[1] <= 127; |
| 39 | return 0 if $o[0] == 0; |
| 40 | return 0 if $o[0] >= 224; |
| 41 | return 0 if $o[0] == 192 && $o[1] == 0 && $o[2] == 2; |
| 42 | return 0 if $o[0] == 198 && $o[1] == 51 && $o[2] == 100; |
| 43 | return 0 if $o[0] == 203 && $o[1] == 0 && $o[2] == 113; |
| 44 | return 1; |
| 45 | } |
| 46 | |
| 47 | push @hits, "private-key-material" if $line =~ /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/; |
| 48 | push @hits, "aws-access-key" if $line =~ /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/; |
| 49 | push @hits, "github-token" if $line =~ /\b(?:github_pat_[A-Za-z0-9_]{20,}|gh[pousr]_[A-Za-z0-9_]{20,})\b/; |
| 50 | push @hits, "slack-token" if $line =~ /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/; |
| 51 | push @hits, "openai-key" if $line =~ /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/; |
| 52 | push @hits, "google-api-key" if $line =~ /\bAIza[0-9A-Za-z_-]{20,}\b/; |
| 53 | push @hits, "jwt" if $line =~ /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/; |
| 54 | push @hits, "credentialed-url" if $line =~ m{[a-z][a-z0-9+.-]*://[^/\s:@]+:[^/\s:@]+@}i && $line !~ m{x-access-token:(?:TOK|TOKEN|REDACTED|EXAMPLE|PLACEHOLDER|YOUR[_-]?TOKEN)\@github\.com}i; |
| 55 | push @hits, "bearer-token" if $line =~ /\bBearer\s+[A-Za-z0-9._~+\/=-]{16,}\b/i && $line !~ /\b(REDACTED|EXAMPLE|PLACEHOLDER|YOUR[_-]?(?:TOKEN|ACCESS[_-]?TOKEN|BEARER[_-]?TOKEN))\b/i; |
| 56 | |
| 57 | if ($line =~ /\b(?:pass(?:word)?|passwd|pwd|api[_-]?key|secret|token|client[_-]?secret|private[_-]?key|access[_-]?key)\b\s*[:=]\s*["'\''`]?([^"'\''`\s<>{}\[\]&,]{8,})/i) { |
| 58 | my $value = lc $1; |
| 59 | push @hits, "credential-assignment" unless $value =~ /^(redacted|example|placeholder|changeme|change-me|xxx|xxxx|null|none|your[_-]?|dummy|sample|fake|test)/ || $value =~ /^\$/ || $value =~ /^(config|settings|options|opts|env|process\.env|os\.environ)\./ || $value =~ /^[a-z_][a-z0-9_.]*(token|secret|key|password)[a-z0-9_.]*$/ || $value =~ /^(tok|token)\@github\.com\b/; |
| 60 | } |
| 61 | |
| 62 | if ($line =~ /\b(?:snmp[_-]?)?(?:community|community[_-]?string|rocommunity|rwcommunity)\b\s*[:=]\s*["'\''`]?([^"'\''`\s<>{}\[\]]{3,})/i) { |
| 63 | my $value = lc $1; |
| 64 | push @hits, "snmp-community" unless $value =~ /^(redacted|example|placeholder|changeme|change-me|xxx|xxxx|null|none)$/; |
| 65 | } |
| 66 | |
| 67 | if ($line =~ /\b(?:customer|client|tenant|account|organization|org|community[ _-]?member)[ _-](?:name|id|identifier)\b\s*[:=]\s*["'\''`]?([^"'\''`<>\[\]{}][^"'\''`<>\[\]{}]{2,})/i) { |
| 68 | my $value = $1; |
| 69 | $value =~ s/^\s+|\s+$//g; |
| 70 | push @hits, "customer-or-private-identifier" unless $value =~ /^(redacted|example|placeholder|customer-|client-|tenant-|account-|org-|user|none|null)/i; |
| 71 | } |
| 72 | |
| 73 | if ($line =~ /\b[A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,}\b/i) { |
| 74 | push @hits, "email-address" unless $line =~ /\b(example\.com|example\.org|example\.net|localhost)\b/i || $line =~ /\bgit\@github\.com[:\/]/i || $line =~ /x-access-token:(?:TOK|TOKEN|REDACTED|EXAMPLE|PLACEHOLDER|YOUR[_-]?TOKEN)\@github\.com/i; |
| 75 | } |
| 76 | |
| 77 | if ($line =~ /\b(customer|client|tenant|account|community member|support|production|prod|log|trace|request|source ip|remote ip|x-forwarded-for|host ip)\b/i) { |
| 78 | while ($line =~ /\b((?:\d{1,3}\.){3}\d{1,3})\b/g) { |
| 79 | push @hits, "public-ip-address" if is_public_customer_ip($1); |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | for my $hit (@hits) { |
| 84 | print "$ARGV:$.:$hit\n"; |
| 85 | } |
| 86 | ' "$file" |
| 87 | } |
| 88 | |
| 89 | for file in "$@"; do |
| 90 | if [ ! -f "$file" ]; then |
| 91 | echo "missing file: $file" >&2 |
| 92 | failures=$((failures + 1)) |
| 93 | continue |
| 94 | fi |
| 95 | |
| 96 | if ! hits=$(scan_sensitive_file "$file"); then |
| 97 | echo "failed to scan file: $file" >&2 |
| 98 | failures=$((failures + 1)) |
| 99 | continue |
| 100 | fi |
| 101 | |
| 102 | if [ -n "$hits" ]; then |
| 103 | printf '%s\n' "$hits" |
| 104 | failures=$((failures + 1)) |
| 105 | fi |
| 106 | done |
| 107 | |
| 108 | exit "$failures" |