Class: Scryer::Rules::HardcodedSecretRule

Inherits:
Scryer::Rule show all
Defined in:
lib/scryer/rules/hardcoded_secret_rule.rb

Overview

Flags plain string literals assigned to a constant/variable/hash-key whose name looks secret-ish (API_KEY, secret, token, password, ...), OR that match a known cloud-provider key format (AWS access key, generic high-entropy-looking tokens), regardless of variable name.

Constant Summary collapse

NAME_PATTERN =
/(api[_-]?key|secret|token|password|passwd|access[_-]?key|private[_-]?key|auth)/i.freeze
KNOWN_KEY_PATTERNS =

High-precision patterns: distinctive enough prefixes/formats that a match alone (regardless of variable name) is worth flagging.

{
  "AWS Access Key ID" => /\bAKIA[0-9A-Z]{16}\b/,
  "Stripe API Key" => /\bsk_(live|test)_[0-9a-zA-Z]{16,}\b/,
  "GitHub Token" => /\bgh[pousr]_[0-9a-zA-Z]{20,}\b/,
  "Slack Token" => /\bxox[baprs]-[0-9a-zA-Z-]{10,}\b/,
  "Private Key block" => /-----BEGIN (RSA |EC )?PRIVATE KEY-----/
}.freeze
WEAK_KEY_PATTERNS =

Low-precision heuristics: only meaningful when ALSO paired with a suspicious variable/constant name (a bare 40-char base64-ish string is just as likely to be a git SHA, a hash, or a test fixture id).

{
  "AWS Secret Access Key (heuristic)" => /\A[A-Za-z0-9\/+=]{40}\z/
}.freeze
PLACEHOLDER_VALUES =

Placeholders/examples that should never be flagged even if they match a pattern above or a suspicious name — avoids noisy false positives on obviously-fake sample values.

/\A(x+|0+|change-?me|your[_-]?(api[_-]?)?key|placeholder|example|dummy|fake|test|redacted|\*+)\z/i.freeze

Instance Attribute Summary

Attributes inherited from Scryer::Rule

#file, #sexp, #source

Instance Method Summary collapse

Methods inherited from Scryer::Rule

inherited, #initialize

Constructor Details

This class inherits a constructor from Scryer::Rule

Instance Method Details

#scanObject



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/scryer/rules/hardcoded_secret_rule.rb', line 37

def scan
  findings = []

  Ast.each_node(sexp) do |node|
    next unless Ast.tagged?(node, :assign)

    target = node[1]
    value_node = node[2]
    value = Ast.plain_string_value(value_node)
    next unless value
    next if value.strip.empty? || value.length < 6
    next if PLACEHOLDER_VALUES.match?(value.strip)

    name = target_name(target)
    matched_known = KNOWN_KEY_PATTERNS.find { |_label, pattern| pattern.match?(value) }
    matched_weak = WEAK_KEY_PATTERNS.find { |_label, pattern| pattern.match?(value.strip) }
    suspicious_name = name && NAME_PATTERN.match?(name)

    next unless matched_known || suspicious_name || (matched_weak && suspicious_name)

    line = Ast.line_of(node)
    reason =
      if matched_known
        "matches the format of a #{matched_known[0]}"
      elsif matched_weak && suspicious_name
        "matches the format of a #{matched_weak[0]} and is assigned to `#{name}`"
      else
        "is assigned to `#{name}`, a name that suggests a credential"
      end
    findings << finding(
      line: line,
      message: "A literal string #{reason} — secrets committed to source control end up " \
                "in git history permanently, even if removed later.",
      suggested_fix: "Move this value out of the codebase: use `ENV.fetch(\"#{env_name(name)}\")` " \
                      "or Rails encrypted credentials (`Rails.application.credentials.dig(...)`), " \
                      "set the real value via your deploy environment / secrets manager, and " \
                      "rotate this specific key since it's likely already exposed in git history."
    )
  end

  findings
end