Module: Sharekit::Cli::Redactor
- Defined in:
- lib/sharekit/cli/redactor.rb
Overview
Strips secret material out of a line before it can leave the machine.
Redaction runs against the original line, never a Finding's truncated preview: truncation can cut a secret mid-token so that the rule pattern no longer matches it, which would let the surviving fragment through.
A masked secret is replaced by its shape (length + Shannon entropy), which is what a classifier actually needs to tell a live key from EXAMPLE_KEY.
Constant Summary collapse
- GENERIC_SECRET_PATTERN =
Backstop for high-entropy material no Scanner rule claims. Runs last, so rule-labelled hits keep their own mask.
%r{[A-Za-z0-9+/=_-]{20,}}- MASK_PREFIX =
"[REDACTED"- ENV_ASSIGNMENT_PATTERN =
Scanner's ENV_VAR_PATTERN ends in
(.*)$, which swallows the rest of the line, so the first assignment is the only one it ever examines. That is fine for reporting one finding per line, but as a redaction pass it leaks: inPATH=/usr/bin API_KEY=shortthe boring first key short-circuits the check and the real secret is never masked, while being too short for the generic backstop. Redaction therefore matches one assignment at a time. Quoted forms are listed explicitly because a bare\S*stops at the space inKEY="a b". /(?:^|\s)(?:export\s+)?([A-Z_]+)=("[^"]*"|'[^']*'|\S*)/
Class Method Summary collapse
-
.entropy(str) ⇒ Object
Shannon entropy in bits per character.
- .mask(secret) ⇒ Object
- .mask_env_values(line) ⇒ Object
-
.redact(line) ⇒ Object
Masks every secret-shaped run in
line: every Scanner rule, then sensitive env-var values, then the generic backstop.
Class Method Details
.entropy(str) ⇒ Object
Shannon entropy in bits per character. Random tokens land near 4+ bits; placeholders like AKIAIOSFODNN7EXAMPLE sit noticeably lower.
40 41 42 43 44 45 46 47 48 |
# File 'lib/sharekit/cli/redactor.rb', line 40 def entropy(str) return 0.0 if str.empty? length = str.length.to_f str.each_char.tally.values.sum do |count| probability = count / length -probability * Math.log2(probability) end end |
.mask(secret) ⇒ Object
34 35 36 |
# File 'lib/sharekit/cli/redactor.rb', line 34 def mask(secret) "#{MASK_PREFIX} chars=#{secret.length} entropy=#{format("%.1f", entropy(secret))}]" end |
.mask_env_values(line) ⇒ Object
59 60 61 62 63 64 65 66 67 68 |
# File 'lib/sharekit/cli/redactor.rb', line 59 def mask_env_values(line) line.gsub(ENV_ASSIGNMENT_PATTERN) do |hit| key = Regexp.last_match(1) value = Regexp.last_match(2) next hit unless Scanner::SENSITIVE_KEY_PATTERN.match?(key) next hit if value.empty? || value.start_with?(MASK_PREFIX) hit.sub(value, mask(value)) end end |
.redact(line) ⇒ Object
Masks every secret-shaped run in line: every Scanner rule, then
sensitive env-var values, then the generic backstop. All rules run, not
just the one that produced the finding, so a second secret sharing the
line cannot ride along into a prompt.
27 28 29 30 31 32 |
# File 'lib/sharekit/cli/redactor.rb', line 27 def redact(line) masked = Scanner::RULES.reduce(line) do |acc, rule| acc.gsub(rule.pattern) { |hit| mask(hit) } end mask_env_values(masked).gsub(GENERIC_SECRET_PATTERN) { |hit| mask(hit) } end |