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
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ jobs:
echo "$out"
echo "$out" | grep -q "0 finding(s)"

- name: Context scoring is switchable (2 findings on, 1 off)
run: |
printf 'phone: (415) 555-2671\ncard 4532015112830366 charged\n' > /tmp/ctx.txt
on=$(/tmp/pii-scan -mode files -level warning -threshold 0.8 \
-context=true /tmp/ctx.txt)
echo "$on"
echo "$on" | grep -q "2 finding(s)"
off=$(/tmp/pii-scan -mode files -level warning -threshold 0.8 \
-context=false /tmp/ctx.txt)
echo "$off"
echo "$off" | grep -q "1 finding(s)"

self-scan:
name: Scan this PR with the action itself
if: github.event_name == 'pull_request'
Expand Down
6 changes: 6 additions & 0 deletions .pii-allowlist
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@
4532015112830366
jane@example.com
536-90-4399

# Context-scoring fixtures: reserved-range values used to show that a labelled
# match outscores a bare one. 555-01xx numbers and RFC 1918 addresses.
(415) 555-2671
415-555-2671
192.168.1.44
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ its own report comments, so it never scans itself in a loop.
| `threshold` | `0.8` | Minimum confidence in `[0,1]`. Raise for precision, lower for recall |
| `entities` | all 45 | Comma-separated entity types to restrict to, e.g. `CREDIT_CARD,EMAIL_ADDRESS,US_SSN` |
| `ignore-entities` | `DATE_TIME,URL` | Entity types dropped as noise |
| `context-scoring` | `true` | Score a match higher when a word naming its entity type precedes it. `false` scores on pattern strength alone |
| `allowlist-file` | – | Repo path with allowed values, one per line (`#` comments ok) |
| `exclude` | `go.sum,*.lock,*.svg,*.min.js,vendor/**,node_modules/**` | Glob patterns of diff paths to skip (`mode: diff`) |
| `comment` | `true` | Post/update the sticky report comment |
Expand Down Expand Up @@ -148,8 +149,14 @@ and point the action at it with `allowlist-file: .pii-allowlist`. To narrow
what's detected instead, set `entities` to the types you care about, or tune
`threshold`: the default `0.8` keeps checksum-verified identifiers (score
`1.0`: credit cards, national IDs, IBANs) and drops shape-only matches.
Lower it (e.g. `0.4`) to also catch emails and phone numbers, which score
`0.5`.
Nearby context words lift a match's score, so a labelled `phone: ...` or
`ip=...` clears `0.8` where the bare value would not. Lower it (e.g. `0.4`)
to also catch unlabelled emails and phone numbers, which score `0.5`.

If you'd rather `threshold` mean pattern strength alone — `0.8` as "checksum
validated, no exceptions" — set `context-scoring: false`. That restores the
scores a pattern earns on its own, at the cost of the real PII the
surrounding words would otherwise reveal.

## What gets posted

Expand Down
10 changes: 9 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ inputs:
ignore-entities:
description: "Comma-separated entity types dropped as noise."
default: "DATE_TIME,URL"
context-scoring:
description: >-
Score a match higher when a word naming its entity type precedes it
(e.g. "phone: 415-555-2671" scores 0.85, the bare number 0.5). Set to
"false" to score on pattern strength alone, so `threshold` means only
how strong the pattern is.
default: "true"
allowlist-file:
description: "Repo path of a file with allowed values, one per line (# comments ok)."
default: ""
Expand Down Expand Up @@ -74,6 +81,7 @@ runs:
THRESHOLD: ${{ inputs.threshold }}
ENTITIES: ${{ inputs.entities }}
IGNORE: ${{ inputs.ignore-entities }}
CONTEXT: ${{ inputs.context-scoring }}
ALLOWLIST: ${{ inputs.allowlist-file }}
EXCLUDE: ${{ inputs.exclude }}
FAIL: ${{ inputs.fail-on-findings }}
Expand All @@ -90,7 +98,7 @@ runs:
echo "marker=$MARKER" >> "$GITHUB_OUTPUT"

args=(-threshold "$THRESHOLD" -ignore "$IGNORE" -level "$LEVEL" \
-report "$REPORT" -marker "$MARKER")
-context="$CONTEXT" -report "$REPORT" -marker "$MARKER")
[ -n "$ENTITIES" ] && args+=(-entities "$ENTITIES")
[ -n "$ALLOWLIST" ] && args+=(-allowlist-file "$ALLOWLIST")

Expand Down
4 changes: 4 additions & 0 deletions cmd/pii-scan/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func run() error {
source := flag.String("source", "", "label for what was scanned, used in the report")
marker := flag.String("marker", defaultMarker, "sticky-comment marker id embedded in the report")
level := flag.String("level", "error", "annotation level: error, warning, or notice")
useContext := flag.Bool("context", true, "score a match higher when a word naming its entity type precedes it (-context=false for pattern-only scores)")
flag.Parse()

allowList, err := readAllowlist(*allowlistFile)
Expand All @@ -46,6 +47,9 @@ func run() error {
}
s := newScanner(*threshold, splitList(*entities), splitList(*ignore), allowList)
s.exclude = splitList(*exclude)
if !*useContext {
s.disableContext()
}

var findings []Finding
switch *mode {
Expand Down
10 changes: 10 additions & 0 deletions cmd/pii-scan/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ func newScanner(threshold float64, entities, ignore, allowList []string) *scanne
}
}

// disableContext turns off context-aware scoring, restoring the scores a
// pattern gets on its own.
//
// This exists for callers who read a threshold as a statement about pattern
// strength alone — "0.8 means checksum-validated" — since context can lift a
// labelled email or phone from 0.5 to 0.85 and over that line.
func (s *scanner) disableContext() {
s.engine.SetContextEnhancer(nil)
}

// excluded reports whether a diff path matches any exclude pattern. Patterns
// match the full path or its basename; "dir/**" matches everything under dir.
func (s *scanner) excluded(file string) bool {
Expand Down
43 changes: 43 additions & 0 deletions cmd/pii-scan/scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,49 @@ func TestScanFile(t *testing.T) {
}
}

// TestContextScoring pins the behavior alcatraz v0.14.0 introduced: a word
// naming the entity type in front of a match lifts its score. The threshold
// here is 0.8, the action's default, so each of these values is the
// difference between a clean scan and a failing one.
func TestContextScoring(t *testing.T) {
tests := []struct {
name string
text string
want int // findings at threshold 0.8 with context scoring on
}{
{"labelled email", "email: jane@example.com", 1},
{"bare email", "ping jane@example.com now", 0},
{"labelled ip", "ip 192.168.1.44 hit the api", 1},
{"bare ip", "saw 192.168.1.44 today", 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := newScanner(0.8, nil, []string{"DATE_TIME", "URL"}, nil)
findings, err := s.scanText(strings.NewReader(tt.text))
if err != nil {
t.Fatal(err)
}
if len(findings) != tt.want {
t.Errorf("context on: got %d findings, want %d: %+v",
len(findings), tt.want, findings)
}

// Pattern-only scoring never rates these above 0.8, so
// disableContext must drop them all regardless of labelling.
s = newScanner(0.8, nil, []string{"DATE_TIME", "URL"}, nil)
s.disableContext()
findings, err = s.scanText(strings.NewReader(tt.text))
if err != nil {
t.Fatal(err)
}
if len(findings) != 0 {
t.Errorf("context off: got %d findings, want 0: %+v",
len(findings), findings)
}
})
}
}

func TestThresholdDropsLowConfidence(t *testing.T) {
// A bare 8-digit run only triggers low-confidence recognizers
// (e.g. US_BANK_NUMBER at 0.05); threshold 0.4 must drop it.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ module github.com/hoophq/alcatraz-action

go 1.24

require github.com/hoophq/alcatraz v0.4.0
require github.com/hoophq/alcatraz v0.14.0
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github.com/hoophq/alcatraz v0.4.0 h1:ZGTvP/eouv7yBatlQx6RIdUQb9gM9gz7ej3RUgPSTsk=
github.com/hoophq/alcatraz v0.4.0/go.mod h1:B5ZNcwna5qCRZpVN6MMjmUbzEDpk5Q04jnKxqfZucdM=
github.com/hoophq/alcatraz v0.14.0 h1:rlmiLv5ko2fG0og71UgPSZ7/77qEF5+bwPYQooRTzSY=
github.com/hoophq/alcatraz v0.14.0/go.mod h1:B5ZNcwna5qCRZpVN6MMjmUbzEDpk5Q04jnKxqfZucdM=
Loading