fix(privacy): drop nvidia-bug-report / dmidecode false positives
- Allowlist nvidia.com, mellanox.com, gnu.org, debian.org, ubuntu.com; the
common "not specified / not available" FRU placeholders.
- Reject matches with no alphanumeric or <2 chars (stray ":" from a dumped
resolv line), fru_location values that echo the field name ("Base Board
Asset Tag", "P1-DIMMA1_AssetTag"), IPv4 embedded in a version string
("18:6.1.4.5"), and e-mail on kernel ring-buffer lines (driver copyright).
- Customer guess: don't report a single-hit low-confidence domain at all -
"unidentified" beats guessing nvidia.com from a driver comment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c9748f3830
commit
551d8e450d
@@ -47,6 +47,13 @@ via `parser.PrivacyScanEnabled()`.
|
||||
a `resolv` finding, not also a `domain` one).
|
||||
- A match that parses as an IP but is not sensitive (see IP policy) is dropped
|
||||
regardless of the rule that produced it.
|
||||
- Noise guards: matches shorter than 2 chars or with no alphanumeric are
|
||||
dropped; `fru_location` values that echo the field name (`Base Board Asset
|
||||
Tag`, `P1-DIMMA1_AssetTag`) are dropped; IPv4 inside a longer dotted-number
|
||||
run (`18:6.1.4.5`) is not an address; e-mail on a kernel ring-buffer line
|
||||
(`[ 8.07] ...`) is driver/copyright text, not customer contact.
|
||||
- Customer guess: a single-hit low-confidence domain is not reported at all
|
||||
(better "unidentified" than guessing `nvidia.com` from a driver comment).
|
||||
|
||||
## Rule catalogue (`rules.go`)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ var (
|
||||
"libssh.org", "rsyslog.com", "adiscon.com", "redhat.com", "kernel.org",
|
||||
"megarac.com", "ami.com", "commond.com",
|
||||
"oasis-open.org", "w3.org", "xmlsoap.org", "purl.org", "ietf.org",
|
||||
"nvidia.com", "mellanox.com", "gnu.org", "debian.org", "ubuntu.com",
|
||||
"inspur.com", "inspurcloud.com", "inservice-iq.com", "ieisystem.com", "kaytus.com",
|
||||
"jd.com", "jd.local", "jdcloud.com", "in-addr.arpa", "ip6.arpa", "arpa",
|
||||
}
|
||||
@@ -35,6 +36,14 @@ var (
|
||||
"null": {},
|
||||
"0": {},
|
||||
"0.0.0.0": {},
|
||||
"not specified": {},
|
||||
"not available": {},
|
||||
"not present": {},
|
||||
"unspecified": {},
|
||||
"no asset tag": {},
|
||||
"no asset information": {},
|
||||
"empty": {},
|
||||
"[empty]": {},
|
||||
}
|
||||
|
||||
// Archive members that are vendor factory templates, not the active config.
|
||||
|
||||
@@ -96,13 +96,15 @@ func guessCustomers(findings []models.PrivacyFinding) []models.CustomerGuess {
|
||||
return guesses[i].Domain < guesses[j].Domain
|
||||
})
|
||||
|
||||
// Keep the strongest few; drop weak single-hit noise unless it is all we have.
|
||||
// Drop weak single-hit low-confidence candidates entirely - a "could not
|
||||
// identify" is more useful than guessing a driver-comment or kernel-source
|
||||
// domain (nvidia.com, linux.it, ...).
|
||||
out := make([]models.CustomerGuess, 0, 3)
|
||||
for _, g := range guesses {
|
||||
if len(out) >= 3 {
|
||||
break
|
||||
}
|
||||
if g.Hits < 2 && g.Confidence == "low" && len(out) > 0 {
|
||||
if g.Hits < 2 && g.Confidence == "low" {
|
||||
continue
|
||||
}
|
||||
out = append(out, g)
|
||||
|
||||
@@ -110,6 +110,44 @@ func TestScan_AllowlistedDomainNotFlagged(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestScan_NvidiaBugReportNoise(t *testing.T) {
|
||||
// dmidecode + dmesg boilerplate that must not be flagged.
|
||||
rep := Scan([]File{{Path: "nvidia-bug-report.log", Content: []byte(
|
||||
"driver bug via the NVIDIA Linux forum (see forums.developer.nvidia.com)\n" +
|
||||
"or by sending email to 'linux-bugs@nvidia.com'.\n" +
|
||||
" Asset Tag: Base Board Asset Tag\n" +
|
||||
" P1-DIMMA1_AssetTag (Date:24/31)\n" +
|
||||
" ME FW Version\n 18:6.1.4.5\n" +
|
||||
"[ 8.078174] pps_core: Software ver. 5.3.6 - Copyright 2005-2007 Rodolfo Giometti <giometti@linux.it>\n")}})
|
||||
if rep != nil && len(rep.Findings) > 0 {
|
||||
t.Fatalf("nvidia boilerplate flagged: %+v", rep.Findings)
|
||||
}
|
||||
if rep != nil && len(rep.Customers) > 0 {
|
||||
t.Fatalf("nvidia boilerplate produced a customer guess: %+v", rep.Customers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScan_RealResolvStillCaughtInNoisyFile(t *testing.T) {
|
||||
rep := Scan([]File{{Path: "nvidia-bug-report.log", Content: []byte(
|
||||
"[ 8.078174] pps_core: <giometti@linux.it>\n" +
|
||||
"--- /etc/resolv.conf ---\ndomain corp.acme.ru\nnameserver 10.0.0.1\n")}})
|
||||
if rep == nil {
|
||||
t.Fatal("nil report")
|
||||
}
|
||||
found := false
|
||||
for _, f := range rep.Findings {
|
||||
if f.Category == catResolv && f.Match == "corp.acme.ru" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("real resolv.conf leak missed: %+v", rep.Findings)
|
||||
}
|
||||
if len(rep.Customers) == 0 || rep.Customers[0].Domain != "acme.ru" {
|
||||
t.Fatalf("customer guess = %+v, want acme.ru", rep.Customers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummary(t *testing.T) {
|
||||
rep := Scan([]File{{Path: "resolv.conf", Content: []byte("domain acme.ru\n")}})
|
||||
if rep.Summary.Total != len(rep.Findings) || rep.Summary.Total == 0 {
|
||||
|
||||
+47
-10
@@ -50,6 +50,9 @@ var (
|
||||
reSyslogTarget = regexp.MustCompile(`@((?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}:[0-9]{1,5})`)
|
||||
reTZName = regexp.MustCompile(`\b((?:Africa|America|Antarctica|Asia|Atlantic|Australia|Europe|Indian|Pacific)/[A-Za-z_]+(?:/[A-Za-z_]+)?)\b`)
|
||||
reTZAbbr = regexp.MustCompile(`\b(MSK|MSD|EEST|EET|MDST|CEST|CET|WEST|WET)\b\s+20\d\d`)
|
||||
// Kernel ring-buffer line: "[ 12.345678] ...". These carry driver/copyright
|
||||
// strings (emails, versions), never customer identity.
|
||||
reKernelTimestamp = regexp.MustCompile(`^\[\s*\d+\.\d+\]\s`)
|
||||
|
||||
tableRules = []tableRule{
|
||||
{catResolv, severityHigh, regexp.MustCompile(`(?i)^\s*(?:domain|search)\s+(\S+)`), 1,
|
||||
@@ -84,15 +87,21 @@ var (
|
||||
func scanLine(path, line string, ln int, certFile bool, emit func(models.PrivacyFinding)) {
|
||||
var local []models.PrivacyFinding
|
||||
specific := map[string]struct{}{} // matches from categories more precise than a bare FQDN
|
||||
kernelLine := reKernelTimestamp.MatchString(line)
|
||||
|
||||
add := func(cat, sev, match, hint string) {
|
||||
match = strings.Trim(strings.TrimSpace(match), `"',;`)
|
||||
if match == "" || isAllowlistedValue(match) {
|
||||
match = strings.Trim(strings.TrimSpace(match), `"',;:`)
|
||||
if len(match) < 2 || !strings.ContainsFunc(match, isAlnum) || isAllowlistedValue(match) {
|
||||
return
|
||||
}
|
||||
if ip := net.ParseIP(match); ip != nil && !isSensitiveIP(match) {
|
||||
return
|
||||
}
|
||||
// BIOS/SMBIOS echoes the field name as the value ("Base Board Asset Tag",
|
||||
// "P1-DIMMA1_AssetTag") - not a customer site tag.
|
||||
if cat == catFRULocation && strings.Contains(strings.ToLower(strings.ReplaceAll(match, " ", "")), "assettag") {
|
||||
return
|
||||
}
|
||||
if cat != catDomain && cat != catEmail && cat != catPublicIP {
|
||||
specific[strings.ToLower(match)] = struct{}{}
|
||||
}
|
||||
@@ -129,21 +138,27 @@ func scanLine(path, line string, ln int, certFile bool, emit func(models.Privacy
|
||||
}
|
||||
add(catDomain, severityHigh, m[1], "domain / FQDN reveals the customer")
|
||||
}
|
||||
for _, m := range reEmail.FindAllStringSubmatch(line, -1) {
|
||||
host := m[1][strings.IndexByte(m[1], '@')+1:]
|
||||
if isAllowlistedDomain(host) || !looksLikeMailHost(host) {
|
||||
continue
|
||||
if !kernelLine {
|
||||
for _, m := range reEmail.FindAllStringSubmatch(line, -1) {
|
||||
host := m[1][strings.IndexByte(m[1], '@')+1:]
|
||||
if isAllowlistedDomain(host) || !looksLikeMailHost(host) {
|
||||
continue
|
||||
}
|
||||
add(catEmail, severityMedium, m[1], "e-mail address")
|
||||
}
|
||||
add(catEmail, severityMedium, m[1], "e-mail address")
|
||||
}
|
||||
for _, m := range reSyslogTarget.FindAllStringSubmatch(line, -1) {
|
||||
add(catCollector, severityMedium, m[1], "remote syslog target")
|
||||
}
|
||||
for _, m := range reIPv4.FindAllStringSubmatch(line, -1) {
|
||||
if !isSensitiveIP(m[1]) {
|
||||
for _, loc := range reIPv4.FindAllStringIndex(line, -1) {
|
||||
if inDottedNumberRun(line, loc[0], loc[1]) {
|
||||
continue // part of a longer version string like "18:6.1.4.5"
|
||||
}
|
||||
ip := line[loc[0]:loc[1]]
|
||||
if !isSensitiveIP(ip) {
|
||||
continue
|
||||
}
|
||||
add(catPublicIP, severityMedium, m[1], "public IP reveals provider / site")
|
||||
add(catPublicIP, severityMedium, ip, "public IP reveals provider / site")
|
||||
}
|
||||
if strings.Contains(strings.ToLower(line), "timezone") || strings.Contains(line, "/") {
|
||||
for _, m := range reTZName.FindAllStringSubmatch(line, -1) {
|
||||
@@ -165,6 +180,28 @@ func scanLine(path, line string, ln int, certFile bool, emit func(models.Privacy
|
||||
}
|
||||
}
|
||||
|
||||
func isAlnum(r rune) bool {
|
||||
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9')
|
||||
}
|
||||
|
||||
// inDottedNumberRun reports whether line[start:end] (a dotted quad) is embedded
|
||||
// in a longer numeric run - a version string like "18:6.1.4.5" or "1.2.3.4.5" -
|
||||
// rather than a standalone address.
|
||||
func inDottedNumberRun(line string, start, end int) bool {
|
||||
if start > 0 {
|
||||
switch line[start-1] {
|
||||
case '.', ':':
|
||||
if start >= 2 && line[start-2] >= '0' && line[start-2] <= '9' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if end < len(line) && line[end] == '.' {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// looksLikeMailHost rejects the many "local@identifier.token" strings that are
|
||||
// not e-mail: OData/Redfish JSON annotations (Members@odata.count), SSH
|
||||
// cipher/kex names (aes256-gcm@openssh.com is handled by the domain allowlist,
|
||||
|
||||
Reference in New Issue
Block a user