Skip to content

Commit d76f5fc

Browse files
Merge pull request google#2261 from doyensec:iban
PiperOrigin-RevId: 937653680
2 parents 1b638d1 + e8ccb41 commit d76f5fc

4 files changed

Lines changed: 480 additions & 0 deletions

File tree

docs/supported_inventory_types.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ See the docs on [how to add a new Extractor](/docs/new_extractor.md).
218218
### Sensitive information
219219
| Type | Extractor Plugin |
220220
| ------------------------------------------- | ------------------------------------ |
221+
| International Bank Account Number | `sensitiveinformation/iban` |
221222
| US Social Security Number | `sensitiveinformation/ssn` |
222223

223224
### Container inventory

extractor/filesystem/list/list.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ import (
184184
"github.com/google/osv-scalibr/veles/secrets/tinkkeyset"
185185
"github.com/google/osv-scalibr/veles/secrets/urlcreds"
186186
"github.com/google/osv-scalibr/veles/secrets/vapid"
187+
"github.com/google/osv-scalibr/veles/sensitiveinformation/iban"
187188
"github.com/google/osv-scalibr/veles/sensitiveinformation/ssn"
188189

189190
cpb "github.com/google/osv-scalibr/binary/proto/config_go_proto"
@@ -456,6 +457,7 @@ var (
456457
})
457458

458459
SensitiveInformationDetectors = initMapFromVelesPlugins([]velesPlugin{
460+
{iban.NewDetector(), "sensitiveinformation/iban", 0},
459461
{ssn.NewDetector(), "sensitiveinformation/ssn", 0},
460462
})
461463

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package iban implements IBAN detection logic.
16+
package iban
17+
18+
import (
19+
"bytes"
20+
"regexp"
21+
"strings"
22+
23+
"github.com/google/osv-scalibr/veles"
24+
"github.com/google/osv-scalibr/veles/sensitiveinformation"
25+
"github.com/google/osv-scalibr/veles/sensitiveinformation/common/simpleregex"
26+
)
27+
28+
const maxSecretLength = 41
29+
30+
// The printed/spaced IBAN format normally groups characters into blocks of four.
31+
// A few countries are exceptions to this and use their own block sizes, so they
32+
// need dedicated alternatives.
33+
//
34+
// https://en.wikipedia.org/wiki/International_Bank_Account_Number#Structure
35+
var ibanRe = regexp.MustCompile(strings.Join([]string{
36+
`(?i)\b(?:`,
37+
// Compact (no spaces, also covers Egypt) or the standard blocks of four.
38+
`[A-Z]{2}\d{2}(?:[A-Z0-9]{11,30}|(?: [A-Z0-9]{4}){2,7}(?: [A-Z0-9]{1,3})?)`,
39+
// Burundi: 4 5 5 11 2
40+
`|BI\d{2} [A-Z0-9]{5} [A-Z0-9]{5} [A-Z0-9]{11} [A-Z0-9]{2}`,
41+
// Libya: 4 3 3 15
42+
`|LY\d{2} [A-Z0-9]{3} [A-Z0-9]{3} [A-Z0-9]{15}`,
43+
// El Salvador: 2 2 4 20 (note the space between country code and check digits)
44+
`|SV \d{2} [A-Z0-9]{4} [A-Z0-9]{20}`,
45+
`)\b`,
46+
}, ""))
47+
48+
// https://en.wikipedia.org/wiki/International_Bank_Account_Number#IBAN_formats_by_country
49+
var countryLengths = map[string]int{
50+
"AD": 24, "AE": 23, "AL": 28, "AT": 20, "AZ": 28,
51+
"BA": 20, "BE": 16, "BG": 22, "BH": 22, "BI": 27, "BR": 29, "BY": 28,
52+
"CH": 21, "CR": 22, "CY": 28, "CZ": 24,
53+
"DE": 22, "DJ": 27, "DK": 18, "DO": 28,
54+
"EE": 20, "EG": 29, "ES": 24,
55+
"FI": 18, "FK": 18, "FO": 18, "FR": 27,
56+
"GB": 22, "GE": 22, "GI": 23, "GL": 18, "GR": 27, "GT": 28,
57+
"HN": 28, "HR": 21, "HU": 28,
58+
"IE": 22, "IL": 23, "IQ": 23, "IS": 26, "IT": 27,
59+
"JO": 30,
60+
"KW": 30, "KZ": 20,
61+
"LB": 28, "LC": 32, "LI": 21, "LT": 20, "LU": 20, "LV": 21, "LY": 25,
62+
"MC": 27, "MD": 24, "ME": 22, "MK": 19, "MN": 20, "MR": 27, "MT": 31, "MU": 30,
63+
"NI": 28, "NL": 18, "NO": 15,
64+
"OM": 23,
65+
"PK": 24, "PL": 28, "PS": 29, "PT": 25,
66+
"QA": 29,
67+
"RO": 24, "RS": 22, "RU": 33,
68+
"SA": 24, "SC": 31, "SD": 18, "SE": 24, "SI": 19, "SK": 24, "SM": 27, "SO": 23, "ST": 25, "SV": 28,
69+
"TL": 23, "TN": 24, "TR": 26,
70+
"UA": 29,
71+
"VA": 22, "VG": 24,
72+
"YE": 30,
73+
"XK": 20,
74+
}
75+
76+
// Most examples taken from https://www.citibank.pl/poland/citidirect/polish/pdf/iban.pdf
77+
var commonExamples = map[string]struct{}{
78+
"AL47212110090000000235698741": {},
79+
"AD1200012030200359100100": {},
80+
"SA0380000000608010167519": {},
81+
"AT611904300234573201": {},
82+
"BE68539007547034": {},
83+
"BA391290079401028494": {},
84+
"BG80BNBG96611020345678": {},
85+
"HR1210010051863000160": {},
86+
"CY17002001280000001200527600": {},
87+
"ME25505000012345678951": {},
88+
"DK5000400440116243": {},
89+
"EE382200221020145685": {},
90+
"FI2112345600000785": {},
91+
"FR1420041010050500013M02606": {},
92+
"GI75NWBK000000007099453": {},
93+
"GR1601101250000000012300695": {},
94+
"GE29NB0000000101904917": {},
95+
"ES9121000418450200051332": {},
96+
"NL91ABNA0417164300": {},
97+
"IE29AIBK93115212345678": {},
98+
"IS140159260076545510730339": {},
99+
"IL620108000000099999999": {},
100+
"KZ86125KZT5004100100": {},
101+
"KW81CBKU0000000000001234560101": {},
102+
"LB62099900000001001901229114": {},
103+
"LI21088100002324013AA": {},
104+
"LT121000011101001000": {},
105+
"LU280019400644750000": {},
106+
"LV80BANK0000435195001": {},
107+
"MK07250120000058984": {},
108+
"MT84MALT011000012345MTLCAST001S": {},
109+
"MR1300020001010000123456753": {},
110+
"MU17BOMM0101101030300200000MUR": {},
111+
"MC1112739000700011111000H79": {},
112+
"DE89370400440532013000": {},
113+
"NO9386011117947": {},
114+
"PL61109010140000071219812874": {},
115+
"PT50000201231234567890154": {},
116+
"CZ6508000000192000145399": {},
117+
"SK3112000000198742637541": {},
118+
"RO49AAAA1B31007593840000": {},
119+
"SM86U0322509800000000270100": {},
120+
"RS35260005601001611379": {},
121+
"SI56191000000123438": {},
122+
"CH9300762011623852957": {},
123+
"SE4550000000058398257466": {},
124+
"TN5910006035183598478831": {},
125+
"TR330006100519786457841326": {},
126+
"HU42117730161111101800000000": {},
127+
"GB29NWBK60161331926819": {},
128+
"IT60X0542811101000000123456": {},
129+
130+
// Additional very common one
131+
"GB82WEST12345698765432": {},
132+
}
133+
134+
// NewDetector returns a Detector that finds International Bank Account Numbers (IBANs).
135+
func NewDetector() veles.Detector {
136+
return simpleregex.Detector{
137+
MaxLen: maxSecretLength,
138+
Re: ibanRe,
139+
FromMatch: func(b []byte, _ bool) (sensitiveinformation.SensitiveInformation, bool) {
140+
if !validIBAN(string(b)) {
141+
return sensitiveinformation.SensitiveInformation{}, false
142+
}
143+
144+
finding := sensitiveinformation.SensitiveInformation{
145+
InfoType: sensitiveinformation.InfoType{
146+
Name: "INTERNATIONAL_BANK_ACCOUNT_NUMBER",
147+
Sensitivity: sensitiveinformation.SensitivityLevelHigh,
148+
},
149+
Likelihood: sensitiveinformation.LikelihoodVeryLikely,
150+
Raw: bytes.Clone(b),
151+
}
152+
153+
return finding, true
154+
},
155+
}
156+
}
157+
158+
func validIBAN(s string) bool {
159+
iban := strings.ToUpper(strings.ReplaceAll(s, " ", ""))
160+
if want, ok := countryLengths[iban[:2]]; !ok || len(iban) != want {
161+
return false
162+
}
163+
164+
if _, ok := commonExamples[iban]; ok {
165+
return false
166+
}
167+
168+
// https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
169+
//
170+
// Opposed to the example described in the Wiki, the algorithm takes 1-digit chunks instead
171+
// of nine. In the case of letter parts of the IBAN, 2-digits chunks are taken, as letters
172+
// map to two decimal digits (A=10..Z=35).
173+
//
174+
// The length of chunks is arbitrary thanks to the properties of modal arithmetics:
175+
// (a⋅10+b) mod 97=((a mod 97)⋅10+b) mod 97
176+
rearranged := iban[4:] + iban[:4]
177+
mod := 0
178+
for _, r := range rearranged {
179+
switch {
180+
case r >= '0' && r <= '9':
181+
mod = (mod*10 + int(r-'0')) % 97
182+
case r >= 'A' && r <= 'Z':
183+
v := int(r-'A') + 10
184+
mod = (mod*100 + v) % 97
185+
default:
186+
return false
187+
}
188+
}
189+
190+
return mod == 1
191+
}

0 commit comments

Comments
 (0)