Changelog¶
All notable changes to this project are documented here. The format is based on Keep a Changelog, and the project aims to follow Semantic Versioning once it reaches 1.0.
This project is pre-1.0: any change to detection thresholds, the asset manifest, or public result shapes may be breaking for downstream users, and is called out explicitly below.
[Unreleased]¶
Added¶
- Add
scripts/l1_threat/generators.py, which derives generated evaluation sets for the threat categories from the frozen corpora: attack texts embedded in realistic carriers (web page, PDF text, ticket, code comment, retrieval chunk, mail), each with a clean twin that holds a benign sentence in the same place; tool output that tells an agent to send data to an outside address, each with a benign look-alike (upload logs, webhook settings, notes addressed to people); and obfuscation slices (base64, hex, rot13, leetspeak, zero-width characters, homoglyphs, letter spacing, Unicode tag characters) over attack and benign texts alike. Tuning and evaluation output use separate seeds and separate template families; the evaluation seed is secret and stored only next to the evaluation data, its manifest records only the seed's SHA-256. A worksheet flow (translate export/translate ingest) turns English tuning rows into German rows translated by hand, with a fixed sample that must be marked as re-checked; every generated row says whether its text is native or translated. Output directories are frozen with a SHA-256 manifest like the corpora; no data is in the repository. Detection change: none; this is evaluation tooling only. - Add a dataset registry for the threat-category evaluation (
scripts/l1_threat/datasets.yaml) andscripts/l1_threat/prepare_corpora.py, which fetches every registered public dataset at a pinned revision, verifies the SHA-256 of each file, maps its upstream labels onto the eleven attack and content categories (a row whose label the mapping does not name stops the run), and freezes one directory per split with a SHA-256 manifest (--dev-out DIR --holdout-out DIR: tuning and report-only data go to the first directory, evaluation data to the second, downloads are cached next to the evaluation data). Datasets are split by source, never at random: each set is either tuning/training data, evaluation data or report-only data. Texts are deduplicated exactly and by MinHash within each set; evaluation rows that repeat a tuning or report row are dropped; every row carries a per-baseline contamination column that marks the training sets of a reference model (for example ProtectAI v2 trained on XSTest and jackhhao/jailbreak-classification) and their duplicates; a training set that a split cannot compare against (pending, or evaluation data while preparing tuning data) is listed as unchecked in the manifest, and unmatched rows are marked unknown for that baseline instead of clean (corpus format v2; a corpus frozen in an older format from the same inputs is rewritten without--force). Each set gets a derived role (decisive, dev, report) from its split, source family and commercial-use flag. Only recipes and manifests are in the repository; no data. Detection change: none; this is evaluation tooling only. -
Add versioned, deterministic text facts
patronus.text_facts/1(patronus_ark_core::detectors::text), the common input for the attack-category models and policies, in the same form as the shell-command facts: the pattern findings of a text (category, family, pattern, points, span) with the modifiers of their clause (decoded,defensive,negated,obfuscated,quoted,test_code), the carrier read from the text structure alone (chat, e-mail, web page, document, tool description, agent configuration, code, log), the addressee (model,authority), sinks (URLs, markdown images, e-mail addresses, sink terms), data references from PII and DLP hits, the obfuscation and decoding facts of the normalisation pre-pass, context terms and the language (de,en,und). The facts have a canonical JSON form that a Python reference (scripts/l1_threat/text_facts.py) reproduces byte for byte, a token analyzertf1(lowercased words plus fact tokens) and a Cedar schema (detectors/text/data/text.cedarschema). Canonical JSON, the strict JSON reader, fact tokens and Cedar schema loading are now shared with shell-command risk (detectors::facts_common); the shell-risk facts, tokens and verdicts are byte-identical. Detection change: none; the text facts are not yet used on the scan path, so scan latency is unchanged. The canonical form keeps the[start, end]order of everyspan(sorting had turned[9, 10]into[10, 9], which the reader refused), andtf1words use Python's\w(letters, numbers,_; no combining marks or joiners) in Rust as well. Detection change: none. -
Add a shared normalisation pre-pass for the attack categories (
patronus_ark_core::detectors::text_views): one pass builds the views a lexicon reads besides the original text — zero-width characters, full-width forms and defanged separators undone, and decoded text hidden in invisible Unicode tag characters, base64/base64url, hex and ROT13 (after aROT13marker) — each mapped back to its original range.TextViews::factsreports two facts,obfuscated(what was undone) anddecoded(which encodings hid prose); digests, token headers, credentials, images and emoji do not count. Decoding is bounded per text. Detection change: base64 and hex payloads in the injection and threat categories are now also decoded when the decoded text contains non-ASCII letters (German umlauts), and the tag characters of subdivision flag emoji (such as the flag of England) are no longer decoded as hidden text. The facts are not yet used by a detector, so scan latency is unchanged. - Detection change: (injection and threat, all modes) the flag exemption applies only when the tags between U+1F3F4 and the cancel tag spell a valid subdivision id (two letters or three digits, then one to four letters or digits); tag-encoded instructions framed by those characters are decoded again.
- The
threatcategory now flags dangerous actions on systems written as prose, code or configuration, in German and English: reading saved browser passwords, the keychain or a password vault; fetching a cloud metadata endpoint; deleting a whole production database or all backups; switching off the firewall, antivirus, audit logging or MFA; downloading a file and running it; granting admin, root or full access to everyone; registering a downloaded or attached program for autostart or as a scheduled task; mounting a container runtime socket or the host root into a container, andprivileged: trueservices in Compose files; deserialising request data withyaml.load,jsonpickle,dill,torch.loadorjoblib; HTTP clients (httpx,aiohttp,urllib3) with certificate checks off against remote hosts. Each of these rules (ark.threat.ua_*, listed inmatched_rules) decides by its own points and the context of its sentence: findings in security documentation and training, quotes, test code, negated sentences, test or staging environments, runs with dry-run or backup, and the user's own narrow scope stay benign; prose requests that need an addressee, a broad scope or a no-confirmation phrase only count with one. Shell commands stay with theshell_commanddetector. The rules can be switched off one by one in therulesmap. Detection change: new threat findings (secrets_access,tool_abuse,harmful_behavior); the decisions of the existing threat rules are unchanged. - Detection change: (all modes with the
threatcategory) the local-machine exemption ofark.threat.ua_unsafe_tls_clientnow checks the parsed destination host, so a loopback name in a path, a longer host name or another argument no longer hides a remote request; andark.threat.ua_untrusted_deserializationno longer flagsyaml.loadwith an explicitly safe loader (SafeLoader,CSafeLoader,BaseLoader,CBaseLoader). - Add clause-wide context lexica for the attack categories in German and English
(
patronus_ark_core::detectors::context_lexica, terms inark-core/src/detectors/anchor_lexicons/context.json): who a text addresses (the model, claimed authorities, including du/Sie/ihr), how far it reaches (scope, sensitive files, external receivers), secrecy, and the counter contexts defensive prompts, security documentation and tutorials, quotation and reported speech, test code and clause-wide negation. Terms cover German compounds, gender forms and separable verbs; every term records where it comes from.scripts/l1_threat/lexicons.pychecks the lexica and proposes candidate terms from a training split. Detection change: none; no detector uses the lexica yet. - Add a latency benchmark for the threat and injection categories on real German text
(
scripts/l1_threat/latency.py).preparebuilds at least 50 distinct windows of exactly 100 KiB from openly licensed German documents (Wikipedia articles, federal laws, Wikimedia policies; pinned revisions and checksums, no paragraph used twice); the texts are downloaded when the corpus is built and never stored in the repository.runtimes the native gateway at L1 withthreatalone,injectionalone and all L1 categories in one request, and reports p50/p95/p99 pooled over the windows;compareexits 1 when a p95 grows by more than 5 % between two reports of the same machine (--profilelimits the verdict to one profile). The runner's latency A/B counts a profile as regressed only when it fails in every attempt, and re-measures only the profiles still failing. The native timing worker (rust/dev/l1_latency_workloads.rs) now accepts the categories to scan per workload. Detection change: none; this is evaluation tooling only.
Changed¶
- Prompt-injection and threat findings now report rule points in each
matched_rulesentry, in the same form PII and secret rules use:rule_score(base points by rule severity, plus the strength of the rule's post-match check, plus 5 points per keyword anchor every match must contain, at most 15),validator_strength(structurefor checks that decode or parse the match, otherwisenone) andcontext(bonus,penalty,anchors). The Rust typepatronus_ark_core::detectors::score::RulePointsholds these points, andScoredPatternhas a newrequired_anchorsmethod (default 0). Detection change: none; verdicts, confidences and spans are unchanged, the points are reported only. - PII and secret (DLP) rules now share one scoring module,
patronus_ark_core::detectors::score: the context points, the validator strength and the score of a finding are defined once for both categories (ScoredPattern,score,rule_score). The formerdetectors::pii::scoreanddetectors::dlp::scoremodules are removed andValidatorStrengthmoves fromdetectors::dlp::dlpanddetectors::pii::scoretodetectors::score; Rust callers update their imports. The 10-point reduction for personal data that only spelled-out normalisation reveals is now the normalisation points of the PII category (Category::normalisation_points). The proximity context engine (windows,Side,Evidence,ProximityLexicon,BEFORE_WINDOW,AFTER_WINDOW) moves fromdetectors::pii::contexttodetectors::contextand serves both categories. Detection change: none; scores, spans andmatched_rulesare unchanged. - Shell commands that the
shell_commandcategory already decides by policy are no longer reported a second time by other detectors. Thethreatcategory drops the rulesark.threat.root_destruction(recursive deletion of/,/etc,/home,/var) andark.threat.host_root_mount(container started with the host root mounted); enableshell_commandto keep blocking these commands. The MCP tool-call policy (dlp_mcp_policy, off by default) no longer flagsnc -e/ncat -e; its other checks are unchanged. Rule toggles (rules: {...}) for the two removed threat rules have no effect any more. Detection change: yes, forthreatanddlp_mcp_policyas described; scores of the remaining threat rules are unchanged. - The MCP tool-call policy (
dlp_mcp_policy) keeps flagging base64 output piped into a shell, appends to shell profiles (including a bare.bashrcin the working directory),history -candunset HISTFILE:shell_commanddecides these only as a bare command, not inside a serialized tool call such as{"tool":"bash","arguments":"history -c"}. Detection change: none against the previous release;dlp_mcp_policykeeps these checks. - The Threat L1 detector now ships in the
patronus-ark-corecrate (patronus_ark_core::detectors::threat::ThreatPipeline), next to the injection detector. Rules, scores and evidence are unchanged, andpatronus_ark::detectors::threatstill resolves to the same detector.
[0.1.16] - 2026-11-11¶
Detection changes in the default modes¶
The default PII and DLP mode is strict; the detailed entries below carry the measurements and
name the modes each change applies to. New PII rules in this release (bare formats, compatibility
rules, letter-prefixed passports) and most document classes are opt-in.
- DLP decoded content: base64 and base64url runs that decode to text are scanned once more
(D01), so provider tokens in Kubernetes
data:values, CI variables and JSON payloads are reported on their encoded span;matched_rulescarriesdecoded. - DLP structure checks: JWTs need a JSON header with
algand JSON claims, private key blocks are checked structurally (DER, OpenSSH, PGP, PuTTY PPK) and reportkey_encrypted; keys filled with example bytes are dropped. - DLP key material on by default: BIP39, Electrum and Monero seed phrases, BIP 32 extended private keys, wallet files, Cardano signing keys and private JWKs and other key material in config and code.
- DLP classification markings on by default: government and corporate markings (VS-NfD, CONFIDENTIAL and similar) are the first document class in the default profile; HR, security, export-control, R&D, corporate and legal document classes are opt-in.
- Latency: three DLP performance passes (no detection change); the 100 KiB DLP p95 is about 8 ms warm in the native pipeline. PII M4 is judged on the median p95 over several runs (T40).
Fixed in this release¶
- The Linux x86-64 wheel no longer crashes in
warmup()for ONNX models; it now needs glibc 2.28 or newer (manylinux_2_28). - Model-backed scanners return correct results on x86-64 CPUs without VNNI.
Breaking¶
- Python: a gateway with
dynamic-piiand amax_levelbelowl3raisesValueError, and scans overdynamic-piiraiseRuntimeErroruntilwarmup()has loaded the model, instead of returning empty results. - Linux wheels need glibc 2.28 or newer.
Added¶
- PII M4 measuring rule (T40, no detection change).
scripts/l1_presidio/latency.py run --runs Nmeasures every engine N times in turn from one build, keeps per engine the run with the median 100 KiB p95 and records every run's p95 (runs_p95_ms). M4 is judged on the median of theark-native100 KiB p95 over at least five runs, at most 12 ms (decision 2026-11-11: the shared VPS spreads a single run by more than 2 ms);--check-m4exits 1 otherwise, also with fewer than five runs. The Python binding is reported next to it and no longer decides M4. - DLP final report and comparison docs (H-FINAL, no detection change). New page
docs/reference/dlp-comparison.md(ARK vs. gitleaks, TruffleHog and detect-secrets on the dev split, breadth counts, coverage matrix incl. Google Cloud DLP and a qualitative Microsoft Purview note), a DLP section in "Choose categories & levels" (default-on groups, opt-in rule gates,dlp_mode), document groups andotp_secretin the DLP taxonomy, andresearch/dlp/scoreboard/final.md(M1–M4).scripts/l1_dlp/milestones.pycounts M3 (gitleaks rules mapped, secret formats, offline structure checks, content classes) fromentity_map.yamland the rule metadata and prints the coverage matrix;scoreboard.py gateprints one aggregatemilestones <split>line (M1, M2 against the pinned baselines) per split. - Detection change: example check on decoded private key octets (H01).
dlp_private_key_blockanddlp_private_key_headerdecode the PEM body (real or escaped line breaks, armor headers; the F-STRUCT parser) and drop a key whose key octets are a repeated fill (EXAMPLEKEY…,TESTKEY…), such as OpenSSH documentation examples; marker words alone do not count on key octets, so a real key with aci@test-runnercomment stays a finding. Dev: the five remaining default-profile hard-negative FP (OpenSSHdoc_example) go to 0, no PEM positive lost; the check runs only on a matched key block.dlp_l1.jsonlgains 1,000 technical hard negatives (hashes, UUIDs, digests, lockfile integrity, commit SHAs, test vectors, docs example keys;patronus_ark.dlp_synth.technical_negatives) and 20 OpenSSH goldens. - Detection change: the H01 fill check reads key octets only (all modes). The comment of an
unencrypted OpenSSH key and the User ID and User Attribute packets of a PGP key are dropped
before the check, so a real key with a repetitive comment (
abcabcabcabcabcabc) stays a finding. - Detection change: opt-in compatibility rules and profile (T38, decision option b).
pii_ipv4_local_compatandpii_ipv6_local_compat(newvalidators::ip_address_local) report loopback and unspecified addresses, also IPv4-mapped and with a prefix length (127.0.0.1,0.0.0.0,::,::1,::ffff:127.0.0.1,::/0; multicast stays out, decision D-IP); a bare::must follow a word at the end of its line.pii_ssn_us_unassigned_compat(newvalidators::us_ssn_unassigned) reports grouped SSNs that the SSA never assigned (area 9xx outside the ITIN groups, the advertising range987-65-4320to4329, the voided078-05-1120and219-09-9999; decision B-SSN). The rules are off by default and, once therulesgate enables them, report like the bare forms; the new profiledocs/how-to/pii-presidio-compat.gates.jsonis the bare profile plus these rules. Validators of the default rules are unchanged;strictandbalancedoutput with the default gates is unchanged. Dev, raw, EN, exact F1 with the profile: presidio_favored IP 86.1 → 98.8 (Presidio default 100, tuned 96.2), US SSN 80.0 → 92.9 (default 100, tuned 88.9); neutral US SSN 82.1 → 80.7 (tuned 79.3). GSTIN and NRIC/FIN gold with invalid check digits and the NINO span with whitespace stay open, so M1 is still not met (no "raw, except …" claim). - Detection change (compatibility profile only, all modes):
pii_ipv6_local_compatalso reports local IPv6 addresses with a leading zero group (0::1,0:0:0:0:0:0:0:1,0:0:0:0:0:0:0:0,0:0:0:0:0:ffff:127.0.0.1), and no longer the subscript of an index or slice in code (items[::1],items[0::1]). The default rules are unchanged. - Detection change: opt-in rules for bare letter-prefixed passport numbers (T37, decision option
c).
pii_passport_number_us_bare(US letter formA12803456,validators::us_passport_number),pii_passport_number_two_letters_bare(two letters and seven digitsXY9876543, newvalidators::passport_number_two_letters_bare: no placeholder run, no EU VAT country prefix) andpii_passport_number_in_bare(Indian formB3097651, newvalidators::in_passport_number: no0after the letter or at the end) report the value without a passport term once therulesgate enables them, then in every mode (format only, base35, thebalancedthreshold0.50); they join the bare profiledocs/how-to/pii-bare-formats.gates.json. Nine bare digits get no bare passport rule (they collide with the bare SSN, SIN, ACN, ABA and BSNR rules), recorded as aknown_gapsentry (T37). With any enabled bare rule, a VAT or identity card term that names the value now drops it (BARE_FOREIGN_LEXICON), and the US and Indian bare forms skip a Spanish CIF afterES. Dev, raw, EN,balanced-bare: passport on presidio_favored 40.0 → 85.7 exact F1 (Presidio default 71.4, tuned 40.0), neutral passport 77.4 → 80.0, ark_authored passport 95.7 → 91.7 (two MRN/patient-ID values), BSNR 76.9 → 80.0 and US SSN 85.7 → 88.2 there. Benign code and docs: no new span.strictandbalancedoutput with the default gates is unchanged. - Detection change: opt-in rule
pii_aba_routing_number_us_barefor the bare ABA routing number (T35). Nine digits (or the printed 4-4-1 form) with a Federal Reserve routing symbol and a valid 3-7-1 check digit (validators::aba_routing_number, unchanged) are reported without a routing or account term once therulesgate enables the rule, then in every mode (base15, thebalancedthreshold0.50); it joins the bare profiledocs/how-to/pii-bare-formats.gates.json. With the default gates the bare digits stay negative (decision B-ACCT), now recorded as aknown_gapsentry. On the same nine digits a default finding of another label wins (SSN: …,BSNR …) and a routing term keeps the account label alone; bare SSN, BSNR and routing findings keep their labels side by side (existing D-BARE order). Dev, raw: account number on presidio_favoredbalanced-bare57.1 → 88.9 exact F1 (Presidio default 66.7;945456787654stays out), neutral account number 49.2 and US SSN unchanged; with the profile the rule adds no span on any other dev row.strictandbalancedoutput with the default gates is unchanged. - Detection change: licence plates in the older UK forms, the Indian form and lower-case Nigerian
plates (T36).
pii_vehicle_registration_uk_prefix(prefix form 1983–2001,A123 BCD,K1 ABC: age letter without I, O, Q, U, Z, serial 1–999, three letters without I, Q, Z),pii_vehicle_registration_uk_suffix(suffix form 1963–1983,ABC 1D, same letter sets, year letter last) andpii_vehicle_registration_in(Indian plate without separators,KA53ME3456: state or union-territory code, district 1–99, one to three series letters without I and O, number other than 0000) report with a plate term near the value inbalancedandrecall, as the Nigerian plate does;strictoutput is unchanged. Their bare forms and the lower-case Nigerian plate (app-456cv, hyphen only) are opt-in rulespii_vehicle_registration_uk_prefix_bare,pii_vehicle_registration_uk_suffix_bare,pii_vehicle_registration_in_bareandpii_vehicle_registration_ng_lowercase_bare(off in every mode, base35, in the bare profiledocs/how-to/pii-bare-formats.gates.json). Both older UK forms need the space as printed; unspaced, the suffix form matched 2,474 hex and base64 runs on the dev benign code corpus. The dateless UK form gets no rule (it matches product and version codes);known_gapsrecords it. Dev, presidio_favored (EN, raw): licence platebalanced-bare72.0 → 93.3 exact F1 (Presidio tuned 81.5),balancedunchanged at 47.6; all languages 80.0 with the profile (TR plates and the short IN formsMN2412,I15432stay without a rule). With the bare profile the new rules add no span on the dev benign code corpus (37.8 MB) and one on benign docs (7.5 MB,GSA 18F); on the PII hard negatives they match only the planted plate values that are negative with the default gates.strictoutput with the default gates is unchanged; the three context-bound rules run inbalanced/recallonly, the bare rules only when enabled. - Detection change: HR, security, export-control and R&D document classes on the M01 content class
engine (M-HRSEC), all opt-in (off by default; the default profile is unchanged). New
dlp_document_hr(dlp.document.hr, M06): employment references (Arbeitszeugnis), warning letters (Abmahnung, written warning), terminations and termination/separation agreements (Kündigungsschreiben, Aufhebungsvertrag), payslips (Entgelt-/Lohn-/Gehaltsabrechnung) and performance reviews (Leistungsbeurteilung). Newdlp_document_security_report(dlp.document.security_report, M13): pentest, security audit, vulnerability assessment and incident reports (Pentest-Bericht, Sicherheitsvorfall, Vorfallsbericht), which must name an affected system (host, server, domain controller, VPN, Zielsystem …). Newdlp_document_export_control(dlp.document.export_control, M16): only labelled identifiers (ECCN, AL-Nummer, Ausfuhrlistennummer or dual-use followed by an Annex I code such as 5A002 or an export list number 0001–0022, EAR99, ITAR-controlled, subject to the ITAR, USML Category). Newdlp_document_rnd_ip(dlp.document.rnd_ip, M17): invention disclosures (Erfindungsmeldung), patent drafts and draft claims before filing (Patententwurf, Anspruchsentwürfe). Each class needs two distinct domain terms and the form of a letter, chat or record within 1,500 bytes of the title. Job ads, law and regulation texts (this Act, im Sinne dieses Gesetzes, Annex I), public advisories (affected versions, fixed in, Sicherheitshinweis), published patents (Offenlegungsschrift, Date of publication) and explanations are counter-context, and two bracketed placeholders ([Name],[yes/no]) near the title mark a form. With all four classes enabled the dev benign-document and benign-code corpora flag no document. Latency (scripts/l1_dlp/latency.py, release, default profile, same machine): base 100 KiB p50 11.23 ms, p95 16.23 ms, head p50 11.05 ms, p95 15.29 ms; 4 KiB p95 2.50 → 1.62 ms; no measurable change, the classes are opt-in and their anchors are searched only where a marker matched. - Detection change: wallet keys and wallet files (C-WALLET, C02 + C03), all
CRYPTO_KEYwith subtypeprivate_key, on by default.dlp_bip32_extended_private_keyreports BIP 32 / SLIP-0132 extended private keys (xprv,yprv,zprv, testnettprv,uprv,vprvwith severitytest) after Base58Check over 78 bytes with the version bytes of the prefix, zero parent fields at depth 0, the0x00key lead and a secp256k1 scalar; extended public keys (xpub,tpub) stay silent.dlp_eth_keystore_ciphertextreports thecrypto.ciphertextof an Ethereum keystore v3 only next to akdf(scrypt or pbkdf2) and a 32-bytemacof the same file, so JWE or KMS ciphertexts do not count.dlp_solana_keypairreports a Solana keypair array (64 bytes, at least 24 distinct) only after a key term (keypair, secret/private key,signer,id.json). Each rule has a keyword prefilter and a bounded regex. The golden recipe length of Base58Check segments now reads up to 8 constant leading bytes. - Detection change (all modes, on by default): Electrum seeds, Monero seeds and Cardano signing keys
close the C-WALLET scope (review of #292), all
CRYPTO_KEYwith checksum strength.dlp_electrum_seed(subtypeseed_phrase) reports 12 words of the BIP39 English word list whose HMAC-SHA512 underSeed versioncarries the prefix of an Electrum seed type (01standard,100segwit,1012FA,1022FA segwit).dlp_monero_seed(subtypeseed_phrase) reports the Monero mnemonic of the private spend key: 24 words of the Monero English word list and the CRC-32 checksum word, also over the line breaks ofmonero-wallet-cli. Both report only a whole run of words after a seed term (electrum,monero,seed,mnemonic,recover,Wiederherstellung) on the line or the line before; BIP39 mnemonics without the Electrum prefix, wrong checksum words, shorter or longer runs and words outside the list stay silent.dlp_cardano_signing_key(subtypeprivate_key) reports CIP-5 Bech32 signing keys (ed25519_sk,ed25519e_sk,addr/stake/acct/root/policy/drep_skand_xsk) with a valid checksum and the key length of the prefix (32/64 bytes, 96 for_xsk); verification keys (_vk) stay silent. Keyword prefilters and linear bounded regexes. - Detection change: key material in config and code (C-KEYS). New rules
dlp_jwk_private_key(a flat JSON object withktyand its private member,dfor RSA/EC/OKP orkforoct, as base64url of at least 16 bytes; public JWKs and the RFC 7517/8037 example keys stay silent),dlp_wireguard_private_key(PrivateKey/PresharedKeyof 32 bytes in base64;PublicKeyis no match),dlp_openvpn_static_key(the 16-line V1 block of--genkey secret, inline in<tls-auth>),dlp_ipsec_psk(the quotedPSKof an ipsec.secrets entry),dlp_symmetric_key(16, 24 or 32 bytes as hex, base64, Fernet or a0xbyte array under an AES, HMAC, encryption, cipher, crypto, signing or Fernet key name; NIST SP 800-38A and counting test keys stay silent),dlp_laravel_app_key(base64:and 16 or 32 bytes),dlp_django_secret_key(50 characters of Django's alphabet with a letter, a digit and a symbol;django-insecure-keys have severitytest) anddlp_rails_secret_key_base(128 hex digits). The baseline entitykey.symmetric_keybecomes shared;key.jwk_private_key,key.wireguard_private_key,key.openvpn_static_keyandkey.ipsec_pskare new ARK-only entities. - Detection change (all modes, same findings): the RFC 7517 A.2/A.3 and RFC 8037 A.1 example JWKs
and the NIST SP 800-38A AES keys are K06 documented examples in
research/dlp/documented_values.yaml(SHA-256 only) instead of clear-text lists in the C-KEYS validators. K06 checks a JWK's private member and a symmetric key's bytes in any encoding the rule reads (hex in either case, base64, a0xbyte array), so the findings stay as before. - Detection change: classification markings (M-MARK: M02 + M03 + M23), the first document class
on by default. New entity group
dlp.document.classification_markingwith five rules, one per marking family:dlp_marking_de_government(VS-NUR FÜR DEN DIENSTGEBRAUCH, VS-NfD, VS-VERTRAULICH, GEHEIM, STRENG GEHEIM),dlp_marking_de_corporate(intern, vertraulich, streng vertraulich),dlp_marking_en_corporate(Confidential, Strictly/Highly Confidential, Restricted, Internal Only),dlp_marking_tlp(TLP 2.0 CLEAR/GREEN/AMBER/AMBER+STRICT/RED and TLP:WHITE) anddlp_marking_nato_eu(NATO SECRET/CONFIDENTIAL/RESTRICTED, COSMIC TOP SECRET, the EU RESTREINT UE/EU RESTRICTED … TRÈS SECRET UE/EU TOP SECRET). The marking is the evidence span and the document label where it stands as a banner (upper case, alone on its line apart from markdown or comment decoration; bare INTERNAL/INTERN are no banner) or as the value of a classification key (Classification:,sensitivity,Einstufung:,Vertraulichkeit). Levels quoted in prose, another level of the same family as a marking within 160 bytes (articles, level lists, code enums) and counter-context on the M01 engine (template, example, Vorlage, Beispiel, Seminar) are no label. Default profile (M23): markings on, every other content and document class stays opt-in. No marking span on any dev corpus (benign code, benign docs incl. Wikipedia and German law texts, external, synthetic carriers). Latency (release, same machine, default profile with the five marking rules gated off → on, 100 KiB benign code with carrier rows): 100 KiB p50 9.32 → 9.20 ms, p95 14.83 → 12.98 ms, 4 KiB p95 0.79 → 0.78 ms; no measurable change. - Detection change (all modes, default profile): a bare level of the same family on a list-shaped
line (
CONFIDENTIAL,/INTERN;) also vetoes a marking within 160 bytes, so the last member of a code enum without a trailing separator is no banner. - Detection change: corporate document classes on the M01 content class engine (M-CORP), opt-in
like every content class; the default profile is unchanged.
dlp_document_unpublished_financials(dlp.document.unpublished_financials, M07: "inside information", "insider list", "MNPI", "unpublished results", "before publication", "Insiderinformation", "Insiderliste", "unveröffentlichte Quartalszahlen", "vor der Veröffentlichung" next to revenue, guidance, consensus, quarter, MAR, Umsatz, Prognose),dlp_document_m_and_a(dlp.document.m_and_a, M08: term sheet, letter of intent, Absichtserklärung, due diligence, data room, Datenraum, share/asset purchase agreement, Anteilskaufvertrag, cap table next to purchase price, buyer, exclusivity, valuation, Kaufpreis, Zielgesellschaft) anddlp_document_board_minutes(dlp.document.board_minutes, M09: board minutes, minutes of the supervisory board meeting, draft/written/board resolution, Aufsichtsratsprotokoll, Protokoll der Vorstandssitzung, Beschlussvorlage, Umlaufbeschluss next to quorum, vote, chair, Aufsichtsrat, einstimmig). The corporate classes need two distinct domain terms near the marker (minimum score 8). Distribution footers of published ad hoc releases (EQS/DGAP, "end of announcement"), annual report and governance headings, press releases, job ads, governance policies ("shall", policy, bylaws) and the shared definition/template/course counters subtract; encyclopedic hedges (typically, usually, such as, in der Regel, üblicherweise) are counter-context of all three corporate classes, so the dev benign-document corpus (Wikipedia articles on M&A and insider trading, Wikimedia board policies) flags no document. The content class engine takes up to 256 anchors per class (was 128). Latency (scripts/l1_dlp/latency.py, release, default profile, same machine as M01): 100 KiB p50 9.74 ms, p95 14.24 ms (M01 head 9.48 / 15.38 ms), 4 KiB p95 1.94 ms; no measurable change, the classes are opt-in and their anchors are searched only where a marker matched. The legal class shares its letter-structure and generic counter lists with them (same anchors as before). - Detection change: legal document classes on the M01 content class engine (M-LEGAL), all opt-in
(off by default; the default profile is unchanged). New
dlp_document_nda_trade_secret(dlp.document.nda_trade_secret, M04): non-disclosure/confidentiality agreements, NDA, trade secret markers, Geheimhaltungs-/Vertraulichkeitsvereinbarung, Geschäfts-/Betriebsgeheimnis, GeschGehG and embargo periods (embargoed until, Sperrfrist) next to parties, confidential information, disclosure duties or press-release terms. Newdlp_document_contract(dlp.document.contract, M22): customer and supplier contracts (Master Services/Supply/License Agreement, Rahmen-, Liefer-, Dienstleistungs-, Lizenzvertrag …) only when at least four contract terms (parties, remuneration, term, termination, confidentiality, liability) stand within 3,000 bytes of the title.dlp_document_legal_privileged(M05) also reports settlement negotiation markers (WITHOUT PREJUDICE, for settlement purposes only, ohne Präjudiz) under the same legal-anchor gate; "ohne Anerkennung einer Rechtspflicht" is no marker, since press and encyclopedic reports of settlements use it. Published laws, court decisions (Leitsatz, Urteil, the court held), contract templates (Muster, Vorlage, [Party A]) and explanations are counter-context. Latency (scripts/l1_dlp/latency.py, release, same machine as M01): 100 KiB p50 9.53 ms, p95 13.66 ms (M01: 9.48 / 15.38 ms), 4 KiB p95 1.98 ms; no measurable change. - Detection change (opt-in NDA/trade secret and contract classes, all modes): both classes also need
document form (
min_structural): a letter or chat (Subject:, Betreff:, Dear, Hallo, please, bitte), agreement wording (this agreement, Effective date, § 1, hereinafter, nachfolgend), a handling instruction (do not forward, nicht weiterleiten) or a record label. The engine counts a marker or medium anchor used as a field label (Sperrfrist:,Term:,"fees":) as record structure. Encyclopedia articles on NDAs and on mergers have the domain terms in prose but no such form; with all gates enabled the dev benign-document corpus went from 34 legal-class hits in 2 documents (NDA article 32, M&A article 2) to 0. Typically, commonly, also known as and their German counterparts are NDA counter-context; between, zwischen, EUR, USD, Datum and signed are no contract anchors any more, since they stand in any prose about deals. - Detection change (opt-in contract, NDA/trade secret and legal-privileged classes, all modes): the
content class engine can require elements (
ContentClass::requiring), groups of anchors of which each needs one hit near the marker.dlp_document_contractneeds contracting parties, remuneration, term and confidentiality (minimum score 14 → 12), so a signed cover letter with exactly these four holds and a meeting note listing customer, supplier, contractor and licensee roles does not.dlp_document_nda_trade_secretneeds a secrecy duty (confidential information, receiving/disclosing party, non-public, need-to-know, do not forward, nicht öffentlich, Geheimhaltung, bleibt intern); newsletters on trade secret rulings or GeschGehG reforms, SECURITY.md embargo notes, job ads ("Candidates sign an NDA") and shop return notes (Sperrfrist für Rücksendungen) no longer pass on letter words and a generic term; newsletter is NDA counter-context.dlp_document_legal_privilegedalso reports draft pleadings (draft complaint, draft statement of claim/defence, draft pleading/petition/motion, Klageentwurf, Entwurf Klageschrift/Klageerwiderung/Schriftsatz) under the same legal-anchor gate. All three classes stay opt-in, so the default profile and its latency are unchanged; the element check is one bit test per element on the anchors the gate already collects. - Detection change (opt-in NDA/trade secret class, all modes): bare confidentiality is no NDA anchor and no secrecy duty, only "confidentiality terms" is; access and data policies that name the NDA their volunteers sign next to confidentiality as a value no longer pass.
- Detection change: content class engine for document classes (M01). A document class is a rule
whose regex matches a strong anchor (a marker); that match is the evidence span and the document
label. The label holds only when the anchors within 1,500 bytes around the marker add up (minimum
density): strong marker 3 points, each distinct medium anchor (domain term) 2, each structural
anchor (Subject:, Betreff:, Dear, Mit freundlichen Grüßen) 1, each counter-context anchor
(example, template, Vorlage, Seminar, definition) minus 3; at least one medium anchor is required.
Anchors are whole-word literals on one Aho-Corasick automaton per class, built once and searched
only where the marker matched, so documents without a marker cost one prefilter keyword lookup.
First class: opt-in
dlp_document_legal_privileged(dlp.document.legal_privileged, attorney-client privileged correspondence DE/EN: "privileged and confidential", "attorney work product", "Anwaltskorrespondenz", "Mandatsgeheimnis", "Verteidigerpost" next to counsel, court, Kanzlei, Rechtsstreit). Off by default like every content class; the default profile is unchanged. Latency (scripts/l1_dlp/latency.py, release, same machine, base → head): 100 KiB p50 9.50 → 9.48 ms, p95 15.26 → 15.38 ms, 4 KiB p95 1.66 → 1.24 ms; no measurable change. - Detection change: DLP decoder pipeline (D01). Canonical base64 and base64url runs (24 to 65 536
characters) whose bytes are printable UTF-8 text are decoded, runs inside the decoded text once
more (recursion depth 2), into one size-limited view (128 KiB per text) that the DLP rules scan
once. A finding of the view is reported on the encoded characters that hold its bytes (every 3
decoded bytes map to 4 encoded characters), its
matched_rulesentry carriesdecoded(the encodings undone, outermost first); a finding of the text itself on the same range keeps its span. Provider tokens in Kubernetesdata:values, CI variables, JSON payloads andecho … | base64 -dlines are now reported; environment references, placeholders, prose, example values and binary data stay silent after decoding. Latency: candidate runs are found by testing each 24-character window from its end and skipping past the byte that breaks it, so most bytes of prose and code are never read; a run is dropped at its first decoded byte that is not text, and the second rule pass covers only the decoded view. - Detection change: JWT and PEM structure (F-STRUCT, F07 + F08 + C07).
dlp_jwt_tokenreports a token only when its header decodes to a JSON object with a stringalgand its claims to a JSON object (validator strengthstructure); unsecured tokens (algnone) with an empty signature are now reported and carryjwt_alginmatched_rules, Supabase and Firebase service tokens carryprovider.dlp_private_key_blockcoversENCRYPTED PRIVATE KEY,PGP PRIVATE KEY BLOCKand PuTTY PPK files besides PKCS#1/PKCS#8/EC/DSA/OpenSSH, checks the body structurally (DER,openssh-key-v1, secret-key packet, PPK key lines; validator strengthstructure) and reportskey_encrypted(OpenSSH bcrypt KDF, encrypted PKCS#8, OpenSSLProc-Type, PPKEncryption).dlp_private_key_headerkeeps a bare header line but drops armor around a body that is no key (..., placeholders, broken DER). authorized_keys/known_hosts lines and public keys stay negative. L1 latency: the checks run only on JWT/PEM matches. - Detection change (all DLP modes): BIP39 seed phrases (C01).
dlp_bip39_seed_phrase(CRYPTO_KEY, subtypeseed_phrase, checksum strength) reports 12, 15, 18, 21 and 24 words of one official BIP39 word list (English, Italian, Portuguese, Czech, Spanish, French) whose checksum (the first ENT/32 bits of SHA-256 of the entropy) holds, written in a line, comma-separated, as a quoted array, numbered (1. word) or one word per line. Spanish and French words count with or without their accents, in NFC or NFD (the unaccented words stay unique per list, as the BIP39 word list notes state). The span runs from the first to the last word. The rule has no literal to search for: one pass over the lowercase words of a text finds runs of list words of one language and checks the windows of a phrase length; when a word shares no language with the run before it, the new run keeps the trailing words that share a language with it, so a phrase whose first word is also on the list of the word before it (seed:before a French phrase) is not cut. A run of more than 27 list words (a word list) is checked only line by line: its lines of 12 to 27 list words may hold phrases (two phrases on consecutive lines, three 12-word phrases one per line, a phrase before a line of list words), a one-word-per-line word list holds none. The published test vectors (trezor/python-mnemonicvectors.json, e.g. the all-zeroabandon … about), phrases that repeat more than two words, uppercase words and runs of 11 words are no phrases; 12- and 15-word phrases (4- and 5-bit checksum) also need numbered words or a context term (seed, mnemonic, recovery, phrase, wallet, backup, passphrase, Wiederherstellung …) in the 200 bytes before them; this length gate counts an accented word once. The checksum check does not allocate per candidate window (stack arrays; the test vectors are decoded once). The CJK lists (Chinese simplified and traditional, Japanese, Korean) are not covered yet. The golden generator gains thebip39_<language>encodings anddlp_l1.jsonlthe Spanish and French positives (bip39_es_12,bip39_fr_18).RuleMeta::with_finderlets a rule match its regex only at the ranges a scanner finds; the catalog names the rule's structural prefilter (an ASCII letter or digit). - L1 PII milestones in the raw view against Presidio default and tuned (M-RAW, owner decision
2026-11-05, supersedes T27 for public claims).
scoreboard.py m1,m2andreportjudge M1, M2 and the F1 >= 80 floor on raw gold (noknown_gapsor validator exclusion); the attainable view stays as a diagnostic column. Every Ark engine (strict,balanced,balanced-bare,recall) is scored against every Presidio profile, and a milestone holds only if Arkbalancedorbalanced-bare(balanced with the opt-in bare profiledocs/how-to/pii-bare-formats.gates.json) beats both Presidio default (threshold 0) and Presidio tuned; the report names the engine that holds (holding). Breaking for the evaluation CLI: the three commands take--ark ENGINE=DIRand--presidio PROFILE=DIRwithdefaultandtunedrequired (report --engineis gone), and the JSON ofm1/m2/reportnests results per Ark engine and Presidio engine (modes.<engine>.presidio-<profile>) with the attainable view underattainable. The gate'smilestones <split> (<engine>, …)lines score balanced and balanced-bare on raw gold against both profiles, one line per engine.run_ark.py --gates PATHapplies therulesof an execution-gates file and records them asengine.rules. Milestone snapshot schema 1.2:pii.milestone_viewandpii.baselinesreplacepii.baseline, anddocs/reference/l1-capabilities.mdshows Ark against each baseline and names the view. Detection change: none; evaluation tooling and docs only, no effect on L1 latency. - The scoreboard gate prints, per split and source family, the exact F1 and the hard-negative FP
count of
strict,balanced,balanced-bare, Presidio default and Presidio tuned (scores <split> (raw gold, …)), and the shared ids wherebalancedorbalanced-barestays below a Presidio profile (M1 gaps <split> (…), entity id and both F1 values, no row id or value). With themilestoneslines this gives the holdout numbers of the final M1–M4 report in the raw view (H05-2,research/l1-pii/scoreboard/final-raw.md). Detection change: none; evaluation tooling and docs only, no effect on L1 latency. - Detection change: opt-in bare-format PII rules (D-BARE). Every value that the default rules
report only with a term of its label or after its field name (
known_gapswithbare: true) has its own rule<rule>_bare:pii_steuer_id_de_bare,pii_steuernummer_de_bare(13-digit ELSTER form),pii_physician_number_lanr_de_bare,pii_facility_number_bsnr_de_bare,pii_driver_license_number_de_bare,pii_ssn_us_bare,pii_sin_ca_bare,pii_aadhaar_in_bare,pii_npi_us_bare,pii_pesel_pl_bare,pii_acn_au_bare,pii_brn_kr_bare,pii_pan_in_bare,pii_voter_id_in_bareandpii_vehicle_registration_ng_bare. They are off in every mode until the existingrulesgate enables them (no new API; example profiledocs/how-to/pii-bare-formats.gates.json). An enabled bare rule keeps the validator of its id, reports in every mode a value that stands alone, outside a key that names another value and without a nearer counter-context term, and scores thebalancedthreshold0.50(base15with a check digit,35format-only), so every enabled bare rule reports inbalancedandrecalltoo; a labelled hit scores at least as high and keeps its span. Default output (all modes, default gates) is unchanged; the goldens are unchanged.NativeGates::all_enabled()includes the bare rules;PiiPipeline::evaluate()keeps the defaults. Measured per rule with only that rule on (new spans against the defaults, strict/balanced): on the 2311 PII hard negatives of goldens and fixture (many are deliberately bare valid values) from 6/6 (pii_steuernummer_de_bare,pii_pan_in_bare) to 64/64 (pii_driver_license_number_de_bare); on the pinned benign corpora (dev split) at most 6.1/MB of code in both modes (pii_facility_number_bsnr_de_bare,pii_ssn_us_bare5.9), at most 0.13/MB of docs. Default-gate latency is unchanged within noise (100 KiB p95, median of four A/B pairs: +1.5 % Python, −8 % native). Per-rule table and commands:research/l1-pii/scoreboard/d-bare.md.
Fixed¶
- DLP L1 latency (H03 pass 3, no detection change): the native threat regexes (the
sensitive_materialandsecret_transferproducers of the DLP category, and the injection and threat families) run once without their assertions over the whole lowercase text before their per-line window searches, and skip the windows when that finds no candidate. About 2,500 per-line searches per 100 KiB document had cost about 2 ms per producer. Results are identical (text_gates_hold_wherever_a_window_matches). Warm DLP gateway 100 KiB p95 on the dev latency workload: 12.1 → 8.0 ms; thelatency.pyp95 (16.6 ms) is still set by the cold first scan of each document (H03-4). Measurements:research/dlp/scoreboard/latency.md. - DLP L1 latency (H03 pass 2, no detection change): a DLP rule that scans the whole text and has
a Unicode word boundary searches a text with non-ASCII characters without its word boundaries
and probes each start anchored with its own regex (the shared
word_boundaryhelpers of the PII H03 pass); the Unicode\bhad sent the lazy DFA to the NFA at the first non-ASCII byte (dlp_url_userinfo_passwordanddlp_url_userinfo_tokenon minified JS bundles: about 370 → 25 ms per 10 MiB repo workload). Config-line values (structure::key_value_line) no longer run the capture engine over the whole line; only the short key prefix runs on a regex. Results are identical (dlp_rule_prefilter,key_value_line_matches_the_full_line_regex). Measurements and the remaining hotspots:research/dlp/scoreboard/latency.md. - DLP L1 latency (H03 pass 1, no detection change): DLP rule regexes and their keyword-window
probes get a 16 MiB lazy-DFA cache (
prefilter::RULE_DFA_CACHE_BYTES, was theregexdefault of 2 MiB). The large case-insensitive rules (dlp_cli_password,dlp_prose_secret,dlp_jwk_private_key) outgrew the default on 100 KiB texts, the cache was cleared again and again and the search fell back to the NFA (a whole-textdlp_cli_passwordscan 28 ms → 0.26 ms per 100 KiB document). Results are identical (dlp_rule_prefilter). Measurements and the remaining hotspots (H03-2):research/dlp/scoreboard/latency.md. -
Detection change (opt-in bare profile only): the bare rules no longer report structured non-PII (T34).
pii_ssn_us_bare(validators::us_ssn_bare) keeps the placeholder filter ofvalidators::us_ssn_context_freeand drops every counting sequence (234567890) and two alternating digits (878787878);pii_facility_number_bsnr_de_bare(validators::bsnr_bare) drops the same placeholders;pii_driver_license_number_de_baredrops an 11-character value in BIC shape (CLABUS33NYC). Every enabled bare rule now also needs a value that stands as a word of prose (no code prefix such asBOL-orNL., no CSV key, no SWIFT, X12 or EDIFACT field separator around it, not right after a SWIFT field tag line), no account, routing, customer or contract term that names it (nearer than its own support term, on the line before, or next to the same value elsewhere in the text), and yields to a finding of another label that a default rule reports on the same characters. On dev,balancedwith the bare profile now equalsbalancedon neutral US SSN (82.1 exact F1, before 46.9) and driver's licence (43.9) and holds M1 neutral 9/9 against Presidio default and tuned; neutral exact F1 69.9 (balanced70.0, before 67.8); presidio_favored M1 19/25 against tuned and M2 89.9 unchanged; ark_authored HN-FP 80. Default validators and the output with the default gates are unchanged; default L1 latency is unchanged within noise (latency.py run --engines ark-native, 100 KiB p95 four runs 8.8–10.8 ms against two runs 9.6 and 10.8 ms without the new overlap step, the only code on the default path). -
Detection change (opt-in bare profile only):
pii_steuer_id_de_bareno longer reports an 11-digit value whose check digit is valid but whose digit distribution no issued Steuer-ID has (Q-RAW). The newvalidators::steuer_id_bareadds the bounds every issued ID meets, old and new BZSt scheme alike (no digit more than three times in the first ten, none three times in a row), without the exact distribution ofsteuer_id_issued, so ten distinct digits (98765432106) still pass and11112345678no longer does. Default output (all modes, default gates) is unchanged and no validator of a default rule changes. On the dev split only (raw gold,scoreboard.py m2), M2 holds with the bare profile (balanced-bare): M2 macro-F1 (German rows, shared and Ark-only ids) onpresidio_favored86.8 → 89.9 against Presidio tuned 87.0 (Steuer-ID 66.7 → 100),neutral62.5 against target 36.2;balancedalone stays at 63.2. Holdout evidence comes from the gate'smilestones holdout (balanced-bare, …)line. M1 stays unmet; the remaining raw gaps per id are listed inresearch/l1-pii/README.md(Q-RAW). No effect on default-gate latency: the bare rule is off by default, and when enabled the extra check runs only on a value whose check digit passed. -
The scoreboard gate printed the raw-gold milestone line only for
balanced, so an M1/M2 claim with the bare profile had no gate evidence (Q-RAW review). It now prints onemilestones <split>line per milestone engine (balanced,balanced-bare) on dev and holdout;balanced-barerunsrun_ark.py --gates docs/how-to/pii-bare-formats.gates.jsonon the head only, and its predictions are cached by the profile's content. Gate only, no detection or latency change. -
The Linux x86-64 wheel segfaulted in
warmup()for every ONNX model (crash instd::regex_traits<char>::transforminside ONNX Runtime). It is now built onmanylinux_2_28instead ofmanylinux2014, so it needs glibc 2.28 or newer (CentOS 7 and Amazon Linux 2 are no longer supported). The release workflow now loads and runs the injection L3 model from the built Linux wheel. - Model-backed scanners returned wrong results on x86-64 CPUs without VNNI (Intel before
Cascade Lake or Alder Lake, AMD before Zen 4): ONNX Runtime's U8S8
MatMulIntegerkernels saturate there, so GLiNERdynamic-piifound no entities and the L3 classifier logits drifted (by up to 9.7 for injection L3 on a Haswell host). On these CPUs every ONNX session now loads its int8 weights rewritten to the equivalent uint8 form; arm64 and VNNI CPUs load the models unchanged. Effect on a Haswell host: GLiNER exact-span F1 on Presidio'ssynth_dataset_v2PERSON 77.5 / LOCATION 61.9 / ORG 54.4 (arm64: 77.7 / 62.2 / 54.1), about +10 % L3 inference time and +0.3 s model load. - Core ships its own
NOTICE(crate and Python package) with the Prompt Armor, pipelock, PromptInject, Garak, DefenseClaw and Cedar attributions of the Core sources; 0.1.15 carried them only in the root and ArkNOTICE. The rootNOTICEpoints sections 3 and 4 at their currentark-core/paths.scripts/release_legal_files.pychecks every packaged license andNOTICEcopy and that Core and root attributions agree; CI runs it on every pull request, so a stale copy no longer surfaces first in the release workflow. - Python:
dynamic-piino longer returns silently empty results. A gateway withdynamic-piiand amax_levelbelowl3raisesValueError(the L3-only GLiNER pipeline never ran there), andscan_all/scan_category/scan_categoriesoverdynamic-piiraiseRuntimeErroruntilwarmup()has loaded its model. Breaking for code that relied on the empty list. - New GLiNER model test (
rust/tests/dynamic_pii.rs, ignored like the other model tests; run withPATRONUS_TEST_GLINER_DIRand--ignored): PERSON, ORGANIZATION and LOCATION must be found in two English and two German sentences.
[0.1.15] - 2026-11-05¶
Detection changes in the default modes¶
The default PII and DLP mode is strict; the detailed entries below carry the measurements and
name the modes each change applies to. Changes that only touch balanced or recall (such as
the M1 work on presidio_favored) are opt-in.
- Shell commands:
shell_commandswitches to the K26 bundle (Cat): verdicts areallow/ask/blockfrom the model plus Cedar policies (built-in, bundle defaults, user.cedarfiles). New asset manifest and result shape; see Breaking below. - DLP provider tokens: new provider rules for LLM providers, Slack/Discord/Stripe formats,
CI and hosting services (P06), observability services (P07), credential files (
.netrc), CLI credentials and credentials in URLs, XML/HTML attributes and DE/EN prose. A value a generic rule reported before now carries the provider rule. - DLP false positives: documented example and test values of the generic credential rules (including decoded base64 values) are dropped without example context; Datadog client tokens (public by design) are no longer reported.
- PII: new and stricter identifier families (IBAN per the SWIFT registry, regional phone numbers, passport, identity card and driver's licence fields, payment fields, and country ids for DE, ES/IT, PL, IN and KR, crypto wallets, device and SIM ids, vehicle and securities ids, US medical licences).
- Latency: the PII and DLP pipelines are compiled once per process, and large texts run their rules in parallel; the 100 KiB p95 stays within the 10 ms budget.
Breaking¶
- One gate schema for Python, the Ark API and Rust (
rules,models,pii_mode,dlp_mode,explain,l1/l2/l3,conditional,l3_policy). - One
SecurityCategorytype for Core and Ark, oneScoreModeinstead ofPiiMode/DlpMode, oneNativeGatesstruct; Core per-call checks are a category selection. - Cat policies are rules
cat.<policy id>in the sharedrulesmap.
Changed¶
- Breaking (API): Python, the Ark API and Rust share one gate schema, the serde
DeserializeofScanGateMatrix(unknown keys refused): nativerules,models,pii_mode,dlp_mode,explain; level switchesl1,l2,l3(booleans);conditional; the L3 worker policyl3_policy. Pythonexecution_gatesdropslevels(use top-levell1/l2/l3), themodel_areasalias and the worker policy underl3(nowl3_policy); the Ark API renamesgates.policytogates.l3_policy. Theexecutionalias ofclustering(worker policy and pipeline overrides) and Python'smin_similarityalias are removed. Out-of-range L3 values (zero counts or costs, fractions outside0..=1) are refused everywhere, in conditionall3_policyoverrides too;ttl_ms/estimated_cost_msentries override the defaults per key in both hosts.L3SchedulerPolicy::validateandL3PipelinePolicy::validateare public; the hand-written Python gate parser and ark-api'sRawGatesare gone. ark-coordinator accepts the same request schema as ark-api (pii_mode,dlp_mode,chunk_overlap_tokens,l1/L1/1levels), checked by a shared fixture. - Breaking (API):
ShellRiskPipeline::verdictandShellPolicies::evaluatetake anallows_rulefilter;ShellConfig::policy_configis replaced byShellPolicyConfig::load(policy_dirs, allowlist).Factsand its parts carry a lifetime (Facts<'a>) so parsed facts borrow their strings. - Cat policies are L1 rules with the id
cat.<policy id>(cat.core.*, bundle-default policies such ascat.CMD-PIPE-CURL,cat.user.*), gated by the samerulesmap as the other L1 rules (CoreScannerConfig,ScanGateMatrix.native, per-request gates, Python, Ark API):falseturns a policy off, a missing id keeps it on, and core policies can be turned off too. Verdicts keep reporting the policy id without the prefix. Disabled regex-gated policies are not evaluated. The L1 rule catalog lists the 117 shipped policies (#shell-command-policies) with tier, scope and level. - Python Core
Scanner.shell_verdicttakesprior_facts(thefactsof earlier verdicts made withwith_facts=True); Rust parses them withFacts::from_json, which round-tripsFacts::to_jsonandcanonical_json. - Full-Ark shell policies outside Rust, with Core's
policy_dirs/allowlistkeys: PythonSecurityGateway.set_shell_command_policy_config(policy_dirs=None, allowlist=None)(refused configuration raisesValueErrorand changes nothing) and the Ark API server configpipeline.shell_command: {policy_dirs, allowlist}, read at startup. - Breaking (API): Core and Ark share one category type.
SecurityCategoryserializes its canonical name and deserializes everyFromStrname, case-insensitively. Core'sCategoryenum is removed:ScannerConfig.categoriesisVec<SecurityCategory>,NativeEngineand its producers takeSecurityCategory(native::NATIVE_CATEGORIESlistspii,dlp,injection), and Core rejects other categories;shell_commandpoints toshellandScanner::shell_verdict. ark-api parsescategoriesby serde with the same names. - Breaking (API): Core per-call checks are a category selection, as in Ark's
scan_categories.ScanChecksandScanner::scan_with_checksare removed; useScanner::scan_categories(text, &[SecurityCategory])andscan_with_control(text, &[SecurityCategory], &ScanControl); Wolf runs iffinjectionis selected. Python:Scanner.scan(text, *, categories=None, control=None)replaces thepii=/dlp=/injection=keywords. - Breaking (API):
PiiModeandDlpModeare oneScoreMode(patronus_ark::ScoreMode,patronus_ark_core::native::ScoreMode); thepii_mode/dlp_modekeys and values are unchanged. - Breaking (API): native rule, model, mode and
explaingates are one Core struct,native::NativeGates, withdefault_rule_enabled(moved fromdetectors::dlp::dlp).ScannerConfig.gatesandScanGateMatrix.nativehold it; the Core JSON/Python config keeps its flat keys. TheNativePolicytrait,ScanGateMatrix::{allows_rule, allows_model}andScanExecution::{allows_rule, allows_model}are removed; call them onNativeGates. SecurityLeveldeserializes everyFromStrname (L1,l1,1, ...); output staysL1.- Breaking (asset manifest, result shape, API):
shell_commandswitches to the K26cat-shell-risk-commanderbundle (revision54617685bc2bc3b7f06ba0c7e7f86c2899940d1f) and itspatronus.verdict/1:max(model, Cedar policies)onallow < ask < block. The detector moves intopatronus-ark-core(detectors::shell, samepatronus_ark::detectors::shellpath). The model's token block is thekf1analyzer (shell tokens, legacy markers,patronus.facts/1tokens). Core policies (core.cedar, compiled in), the translated DefenseClaw rules at their gated levels and user policies from.cedarfiles run on the officialcedar-policycrate. The download addsconfig.jsonandpolicy_levels.json; the release manifest's sha256 is pinned in the binary and every file read is checked against it.layers[0].detailsgainsdecision,bundle_scoreand the fullverdict;variant,full_scoreandcomments_stripped_scoreare gone (K26 scores the command as written).ShellRiskPipeline::scorereturnsResult; theShellRiskPipeline::with_threshold_policymethod, itsthreshold_policyfield and the G-format model loader are removed (the result detaildetails.threshold_policystays);verdict(text, context, prior_facts),configure,from_pinned_bundle_dirandSecurityGateway::set_shell_command_policy_configare new. - Ark Core runs Cat:
ScannerConfig.shell(ShellConfig: bundle directory, manifest pin, user.cedarpolicy directories, allowlist) andScanner::shell_verdict(command, context, prior_facts), in PythonScanner.shell_verdict(command, cwd=..., home=..., ...).Scanner::warmupwithdownload: missingfetches the pinned release (warmup::SHELL_REPOSITORY,SHELL_REVISION,SHELL_FILES,SHELL_MANIFEST_SHA256, which the full runtime'sSHELL_COMMAND_ASSETnow uses). New guide: Check shell commands; new examplesark-core/examples/core_scanner.rsandrust/examples/14_shell_command.rs. detectors::shell::testing::write_synthetic_bundle(featuretest-util) writes the synthetic K26 test bundle; the copies in the gateway and ark-api tests are removed.- Docs: the root README is a component map (Core → Ark → ark-api, ark-coordinator) with
quickstarts, real outputs and one "turn rules on and off" section for every surface; the Core and
Core Python READMEs are rewritten;
rust/README.mdis the new full-Ark README for the crate and thepatronus-arkPyPI package. New guide Check shell commands. Stale pages fixed: the A1 and comment-stripping description ofshell_command, Cat in Core vs. full Ark, the Package-v2 claim in the asset docs, dead rule-catalog anchors (now stable#dlp-regex-rules), the contributor test and release lists and the example index (docs/USAGE.md).ScanError::Assetreadsasset error: …(wasWolf asset error: …), as Cat uses it too. - Core detectors share the
NativeDetectortrait (detect_prepared,detect,evaluate,evaluate_batch); the shell detector implements it. - The native PII and DLP pipelines compile their rules once per process; every engine and gateway shares them instead of compiling all rule regexes again on construction. Detection is unchanged; building a gateway (and the debug test suite, which builds one per scan in many tests) gets much faster.
- Detection change: credential files (G04). New rule
dlp_netrc_password(CREDENTIAL, subtypepassword, structural credential tier) reports the password of a.netrc/_netrcentry (machine <host> login <user> password <value>, also one token per line, and thedefaultentry; the host is a dotted name, an address orlocalhost). New ruledlp_docker_config_auth(CREDENTIAL, subtypepassword) reports theauthvalue of a Docker/Podmanconfig.json/auth.jsonregistry entry, also inside a.dockerconfigjsonsecret, when it decodes touser:passwordwith a chosen password (the Basic-auth check). References, stand-ins, empty and credential-helper entries stay negative..npmrc_authToken/_password,.pypirc, Mavensettings.xml,NuGet.ConfigClearTextPasswordand.s3cfgkeys are already reported by the generic assignment and AWS rules; rclone's obscuredpassis not reported. - Detection change: credentials in SQL dump rows (T23).
INSERT … VALUESstatements that start their line (alsoINSERT IGNORE,INSERT OR REPLACE,REPLACE INTO, several tuples over one or more lines) andCOPY … FROM stdinblocks are read as rows: a column whose name passes the credential key gate (password,kennwort,api_token,client_secret…) binds its field in every row, and a quoted key field of a settings row ((2,'service_secret','…'),'… – Client-Secret') binds the next field, fordlp_generic_credential_assignment,dlp_env_var_secretanddlp_password_assignmentwith their key gate, value gate and value-shape filter (structuresql).dlp_csrf_tokenanddlp_password_hashbind through settings key fields only; apassword_hashcolumn of user digests and a container column (zugangsdaten,credentials) without a credential label bind nothing. SQL built in code strings, DDL,NULL, numbers and expressions (current_setting('…')) bind nothing; anINSERTbinds only when its tuples end in;, as in a dump. A stored password hash under a password key of a row (hex digest, bcrypt, Argon2, SHA-crypt, Django/Werkzeug,{SSHA}…) and a word or identifier in a row field ('testpass'in test seed rows) bind nothing. A placeholder after an auth scheme or a variable name (Bearer HIER_…_EINTRAGEN,NAME=BITTE_AENDERN) is a placeholder for every rule, and a sequential or repeated fill in a SQL row counts as a test value. Devsql.*carrier misses of implemented credential entities 11 -> 0. - Detection change:
dlp_connection_string_passwordanddlp_ldap_bind_password(all modes) report a bare password whose dotted letter runs end in-and a letter or digit (Password=yogRXHK.OY.KvDC-C9;): the member-access check no longer reads it as a subtraction. A whole member access (Password=cfg.DbPassword;) stays code (T23). - Detection change: an SQL dump statement (T23) ends at the next
INSERT/COPYhead at the latest, so aCOPY … FROM stdin;block without\.no longer reads the followingINSERTlines as its rows; many heads without an end cost linear instead of quadratic time. - Detection change: example/test check on decoded base64 values for the generic rules (T22).
dlp_generic_credential_assignment,dlp_env_var_secret,dlp_password_assignment,dlp_prose_passwordanddlp_prose_secretdecode a value that is canonical standard or URL-safe base64 (optionally afterpypi-,glc_orops_) and drop it when a printable run of the decoded bytes holds an example/test marker word or is covered by repeated chunks (Kubernetes Secretdata:values, Grafana legacy keys andglc_tokens, PyPI macaroons, 1Passwordops_tokens, Elasticid:api_keywith anEXAMPLEKEY,TESTKEY,deadbeefor1234567890fill). The fixed-format check of the provider rules applies the same decoded check, sodlp_grafana_api_key, the Grafana Cloud, PyPI and 1Password rules drop such fills as well. A dotted host name in the payload (test.pypi.org) is no marker. Random base64 secrets stay findings. Dev hard-negative FP of the generic rules on these base64 values 274 -> 0. - Detection change: key-bound provider values in XML config elements (T21). The
provider_key!rules (dlp_heroku_legacy_api_key,dlp_railway_token,dlp_netlify_legacy_token,dlp_travis_token,dlp_circleci_legacy_token,dlp_vercel_legacy_token,dlp_azure_devops_legacy_pat,dlp_cloudflare_api_key,dlp_bitbucket_client_secret,dlp_datadog_api_key,dlp_newrelic_legacy_license_key,dlp_sentry_auth_token,dlp_splunk_hec_token,dlp_elastic_api_key,dlp_honeycomb_api_key,dlp_pagerduty_api_key) read the K05C XML binding: a keyed element whosekey/nameattribute ends with the provider name and a credential noun (<add key="HerokuApiKey" value="…"/>in either attribute order,<entry key>,<setting name>) and the text of an element so named (<SplunkHecToken>…</SplunkHecToken>). The value must pass the provider's format, validator and example checks and keeps its provider rule instead ofdlp_generic_credential_assignmentor no finding; IDs under the provider name (HerokuAppId), HTML forms, XML comments and XML in code strings stay silent. Matches carrystructure: "xml". - Detection change: productivity/SaaS tokens (P10). New prefix rules with a token boundary:
dlp_atlassian_api_token(ATATT3and 186 characters[A-Za-z0-9_=-], gitleaks atlassian-api-token),dlp_notion_api_token(ntn_, 11 digits and 35 base62, gitleaks notion-api-token),dlp_linear_api_key(lin_api_and 40 base62, gitleaks linear-api-key),dlp_airtable_pat(pat, 14 base62,.and 64 hex, gitleaks airtable-personnal-access-token),dlp_postman_api_key(PMAK-, 24 and 34 hex, gitleaks postman-api-token) anddlp_mapbox_secret_token(sk., a 50 to 200 and a 22 character segment, TruffleHog Mapbox). Context rules under a provider key with a credential noun (provider_key!):dlp_atlassian_legacy_api_token(24 base36 underatlassian/jira/confluence),dlp_linear_client_secret(32 hex, gitleaks linear-client-secret) anddlp_airtable_api_key(keyand 14 base62, gitleaks airtable-api-key). Random bodies need both letter cases. Generic rules no longer report a public Mapboxpk.token or a token prefix followed by a placeholder (ntn_<your-secret>,lin_api_${KEY}). The GOLD-D1 baseline entitiessaas.atlassian.api_token,saas.notion.api_token,saas.linear.secret,saas.airtable.token,saas.postman.api_tokenandsaas.mapbox.secret_tokenbecome shared; the Mapbox gold now holds secretsk.tokens and the publicpk.token as its lookalike. Figma, Asana, monday.com, HubSpot, Salesforce, Zendesk and Intercom get no new rule in this change. - Detection change: hyperscaler credentials (P03). New rules
dlp_azure_storage_account_key(base64 of 64 bytes afterAccountKey=in a connection string, detect-secrets AzureStorageKeyDetector),dlp_azure_entra_client_secret(three characters, a digit,Q~and 31 to 34 characters with both letter cases, gitleaks azure-ad-client-secret),dlp_alibaba_access_key_id(LTAIand 20 base62, gitleaks alibaba-access-key-id),dlp_alibaba_secret_key(30 base62 under an Alibaba or Aliyun key with a credential noun, gitleaks alibaba-secret-key),dlp_aws_bedrock_api_key(ABSKand base64 that decodes toBedrockAPIKey-…, gitleaks aws-amazon-bedrock-api-key-long-lived) anddlp_ibm_cloud_api_key(44 characters[A-Za-z0-9_-]under an IBM key, detect-secrets IbmCloudIamDetector). The GOLD-D4 baseline entitiescloud.azure.storage_account_key,cloud.azure.entra_client_secret,cloud.alibaba.access_key,cloud.aws.bedrock_api_keyandcloud.ibm.credentialsbecome shared entities with these rules. GCP service account key files, AWS session tokens, Amazon MWS, Azure SAS signatures beyonddlp_signed_url_signature, Tencent Cloud and Oracle OCI get no new rule in this change. - Detection change: LLM/AI provider keys (P02). New prefix rules with a token boundary, each per
TruffleHog:
dlp_langsmith_key(lsv2_pt_orlsv2_sk_, 32 lower-case hex digits,_and 10 lower-case hex digits),dlp_pinecone_key(pcsk_, a 5 or 6 character key ID,_and 63 base62 with both cases) anddlp_elevenlabs_key(sk_and 48 lower-case hex digits). Stability AI has no format the pinned gitleaks or TruffleHog detects and gets no rule. - Detection change: LLM/AI provider context keys (P02 review), all modes. Azure OpenAI, Weights &
Biases, AssemblyAI and Deepgram keys have no own marker; like TruffleHog's keyword detectors, new
context rules bind their lower-case hex values to the provider name:
dlp_azure_openai_key(32 digits under an Azure OpenAI key or inAzureOpenAI(api_key=…)),dlp_wandb_key(40 under a wandb key, afterwandb loginor inwandb.login(key=…)),dlp_assemblyai_key(32 under an AssemblyAI key oraai.settings.api_key) anddlp_deepgram_key(40 under a Deepgram key or inDeepgramClient(…)). IDs under the provider name (WANDB_RUN_ID,DEEPGRAM_PROJECT_ID) stay silent. The values were reported by generic rules under a credential key only, without provider attribution, and not at all in client calls orwandb login. - Detection change: LLM provider keys (P01). New rules
dlp_perplexity_key(pplx-and 48 base62, gitleaks perplexity-api-key),dlp_cohere_key(40 base62 under a Cohere orCO_API_KEYkey with a credential noun, gitleaks cohere-api-token),dlp_openrouter_key(sk-or-v1-and 64 lower-case hex digits),dlp_deepseek_key(sk-and 32 lower-case alphanumerics under a DeepSeek key) anddlp_fireworks_key(fw_and 20 to 40 base62), the last three per TruffleHog; the Perplexity, Cohere and Fireworks bodies need both letter cases. The GOLD-D5 baseline entitiesllm.cohere.api_keyandllm.perplexity.api_keybecome shared entities with these rules. Gemini/AI Studio keys are Google API keys and stay withdlp_google_api_key. Mistral and Together AI keys have no documented format (no gitleaks or TruffleHog rule) and get no rule. - Detection change: provider and CSRF values handed over in DE/EN prose (T20). The key-bound
provider rules (
dlp_heroku_legacy_api_key,dlp_railway_token,dlp_netlify_legacy_token,dlp_travis_token,dlp_circleci_legacy_token,dlp_vercel_legacy_token,dlp_azure_devops_legacy_pat,dlp_cloudflare_api_key,dlp_bitbucket_client_secret,dlp_datadog_api_key,dlp_newrelic_legacy_license_key,dlp_sentry_auth_token,dlp_splunk_hec_token,dlp_elastic_api_key,dlp_honeycomb_api_key,dlp_pagerduty_api_key) anddlp_csrf_tokenalso read the G-PROSE handover of chats, tickets and mails: the provider name and a credential noun on the line or up to two lines before, the value after a colon at the end of its line or alone (the Heroku API key for the job:and the value below it,(Heroku-API-Schlüssel): …,Wie lautet der Wert für „CSRF-Token“?and the reply). The value must pass the provider's own format, validator and example checks, so it keeps its provider rule instead ofdlp_prose_secret, and Elastic keepsid:api_keyas one span. The prose rules and the provider handover drop a value after an ID noun (App-ID:,project id:,Service-ID:). - Detection change: URL query credentials in XML and HTML attribute values (T16).
dlp_credential_in_url,dlp_signed_url_signatureand the query branch ofdlp_connection_string_passwordtake the entities&and&as the parameter separator: anapi_key,tokenorsecretparameter of<add value="https://…&api_key=…">keepsdlp_credential_in_urlinstead ofdlp_generic_credential_assignment, and SigV4, GCS V4, Azure SAS and CloudFront signatures after&are reported. Pagination, cursor, CSRF and signed-URL structure checks are unchanged. - Detection change: URL credentials (G-URL, F12 + G08 + G09).
dlp_credential_in_urlanddlp_generic_credential_assignmentno longer report a URL parameter that is a CSRF parameter, atokennext to a pagination, cursor or CSRF parameter, or thesecretof an otpauth:// URI.dlp_signed_url_signatureneeds signed-URL structure (X-Amz-/X-Goog- companions, CloudFrontExpires/Key-Pair-Id/Policy, Azure SASsvwithseorsp); a baresig=orSignature=is no signature. New ruledlp_auth_url_token(credential.auth_url, subtypeauth_url): OAuth redirect tokens and codes, reset, magic-link, invitation and confirmation tokens in queries and paths. New ruledlp_otp_secret(credential.otp_secret, subtypeotp_secret): Base32 seeds of otpauth:// URIs and after TOTP/2FA secret names with an RFC 4648 length and padding check; the Google Authenticator Key Uri Format example seed is a documented value. 2FA recovery code lists are not covered yet. - Detection change: M1 on presidio_favored (Q-M1),
balancedandrecallonly;strictoutput bit-identical.pii_vehicle_registration_uk_context_freereports the UK current form (AB51 ABC,BD62XYZ) without a plate term, validated byvalidators::uk_vehicle_registration(issued area letters and age identifier), at score 0.50; a nearer counter-context term drops it.pii_vehicle_registration_ngreports the Nigerian plate (three letters, three digits, two letters, joined by a hyphen or a space) with a plate term near the value; a later plate of a list (,,and,und,&) takes the term of the first (Plates: APP-456CV and KJA-999PZ). The plate lexicon gainsplateandplates. Newknown_gapsinresearch/l1-pii/entity_map.yamlfor bare SIN digit runs (B-LUHN), bare twelve-digit Aadhaar (B-IN) and bare ten-digit NPI (A06). Dev, balanced vs Presidio tuned, attainable: M1 presidio_favored 18/22 -> 22/22 (CA SIN 57.1 -> 100, Aadhaar 85.7 -> 100, NPI 88.9 -> 100, licence plate 22.2 -> 76.9 vs 76.9), neutral 9/9; raw licence plate 11.8 -> 47.6. Only the presidio_favored recognizer-test predictions change; validators unchanged. - L1 PII M2 on presidio_favored (Q-M2). A new
known_gapsentry inresearch/l1-pii/entity_map.yamlrecords the bare 13-digit ELSTER tax number (0181508150000) that the Q-DE decision keeps field-bound (bare, 13 digits are as often a millisecond timestamp or an EAN-13); the slashed Länder groups stay gold. Dev, balanced, attainable: tax number 66.7 -> 100 (tuned 100), M2 macro-F1 presidio_favored 96.3 -> 100.0 (tuned 100.0), neutral 66.1 (target 36.2), M2 met. Raw view, gate and validators unchanged. Evaluation only: no detection change, strict and balanced output bit-identical. - L1 PII quality floor on presidio_favored for the country ids added since W44 (T33). New
known_gapsinresearch/l1-pii/entity_map.yamlrecord the gold that existing decisions keep out of reach: bare ACN (B-APAC), bare ten-digit KR BRN (B-KR), bare PESEL (A13), bare IN PAN and voter ID gold without a voter ID term (B-IN), and a Partita IVA with an underscore separator (T33). GSTIN, NRIC/FIN, KR RRN and codice fiscale gold failing the check digit already left the attainable view. Dev, presidio_favored, balanced, attainable: every listed id is at exact F1 >= 80 or keeps no attainable gold (IN PAN 50 -> 100, KR BRN 66.7 -> 100, PESEL 50 -> 100; ACN, voter ID, GSTIN and IT VAT ID without attainable gold); M1 attainable 17/24 -> 18/22. Raw view, gate and validators unchanged. Evaluation only: no detection change, strict and balanced output bit-identical. - Detection change: Slack, Discord and Stripe formats (F-PROV, F09 + F10).
dlp_slack_tokentakes the gitleaks Slack shapes instead ofxox[bpras]-and 15 open characters: bot tokens with numeric team and bot user IDs, legacy bot tokens (one ID, 18 to 26 characters), user tokens (xoxp-/xoxe-, three IDs, 28 to 34 characters), legacyxoxo-/xoxs-tokens with a hex secret and workspacexoxa-/xoxr-tokens (8 to 48 characters). The new ruledlp_slack_app_tokenreports app-level tokens (xapp-, upper-case app ID, 64 hex digits) and app configuration access and refresh tokens (xoxe.xoxp-,xoxe-);comm.slack.app_config_tokenmoves from baseline-only to shared.dlp_discord_tokendecodes the first segment of a bot token to the bot's snowflake ID (17 to 20 digits, no leading zero, within 64 bits; validator strengthstructure) and takesM/N/Oleads, 6-character timestamps and 27 to 38 character HMACs.dlp_stripe_keytakessk_/rk_withlive/testand 20 to 99 base62 characters (underscores only, test keys keep severitytest); publishablepk_keys stay no secret.dlp_stripe_webhooktakeswhsec_with 32 base62 characters (Dashboard) or 64 hex digits (Stripe CLI) instead of 20 or more. Format sources inresearch/dlp/rule_sources.yaml. - Presidio
tunedbaseline enables the off-by-default English recognizers of shared ids (T32, decided 2026-11-02, option a).scripts/l1_presidio/profiles/tuned.yamlnow also runs US NPI, CA SIN, SG NRIC/FIN, ZA ID, AU ABN/ACN/TFN/Medicare, IN PAN/Aadhaar/passport/GSTIN/voter ID and the UK/IN/NG vehicle registration recognizers that Presidio 2.2.364 ships withenabled: false; recognizers ofpresidio_onlyids and of other languages stay off. Dev, balanced vs Presidio tuned: presidio_favored exact F1 tuned 71.3 -> 77.6 (Ark 78.1), neutral 40.6 -> 40.5, no hard negative hits; M1 attainable 24/24 -> 17/24 (AU ACN, CA SIN, Aadhaar, IN PAN, voter ID, licence plate and NPI fall below tuned), M2 unchanged. Evaluation baseline only: no detection change. - L1 PII milestones on attainable gold (T27, decided 2026-11-02, option b).
scoreboard.py m1,m2andreportscore a raw and an attainable view; M1, M2 and the F1 >= 80 quality floor are judged on the attainable one for every family and both engines alike. It leaves out gold that fails every checksum validator of its id (a passport or identity card only the validator of its issuing country, where the row names it) and gold under a recorded decision: the newknown_gapslist inresearch/l1-pii/entity_map.yaml(D-IP loopback, T28 IPv4 part of an invalid IPv6 chain, B-SSN, Gretel's foreignssngold, bare BSNR/LANR/tax ID/DE driving licence, bare TR/IN/NG plates, a NINO span with a leading space), checked byentity_map.py check. The gold-gap report no longer counts every passport as failingde_passport_numberand lists the known gaps per corpus and entity. The gate and the raw view are unchanged; validators are not loosened. Dev, presidio_favored, balanced vs Presidio tuned: M1 22/25 raw, 24/24 attainable; M2 macro-F1 63.2 vs 87.0 raw, 96.3 vs 100.0 attainable. No detection change. - L1 latency loop, pass 6 (H06-6). With
explainon, the context anchor patterns of PII and DLP ((?i)\b…\b) are searched without their word boundaries on the lazy DFA and each candidate start is probed anchored, as the PII rules since H03 (shareddetectors::word_boundarymodule): a Unicode word boundary sent the search to the slow NFA after the first umlaut. Anchor details unchanged; explain on, 100 KiB DE p50 71.9 → 9.6 ms, 4 KiB DE 8.35 → 1.31 ms. Profile of the default path and the measured attempts inresearch/l1-pii/scoreboard/latency.md. M4 not yet met in Python; next pass H06-7. Detection change: none. - L1 latency loop, pass 5 (H06-5). The Python binding hashes its per-call cache of the result
strings (the keys and string values of the
matched_rulesentries, span labels) with Fx instead of SipHash: the 100 KiBdetailsgetter 1.55 → 1.25 ms (fastest call); A/B of the Python call against main, 100 KiB p50 11.12 → 10.55 ms, p95 13.63 → 13.25 ms. 100 KiB p95 on the VPS (scripts/l1_presidio/latency.py): ark 15.170 → 15.003 ms, ark-native 9.803 → 9.774 ms (native code unchanged). M4 not yet met in Python; next pass H06-6. Detection change: none. - L1 latency loop, pass 4 (H06-4). PII rules whose every match is one whole ASCII word run
(
\b[A-Z]{5}[0-9]{4}[A-Z]\b) carry a word shape (length range, allowed and required character kinds); one pass over the text records which word run shapes occur, and a rule whose shape occurs nowhere is not searched (41 rules have a shape). 100 KiB rule search CPU 17.2 → 14.3 ms (DE), 16.7 → 13.9 ms (EN). 100 KiB p95 on the VPS under higher load (scripts/l1_presidio/latency.py): ark 14.641 → 15.170 ms, ark-native 9.947 → 9.803 ms, both within the noise. M4 not yet met in Python; next pass H06-5. Detection change: none. - L1 latency loop, pass 3 (H06-3). The Python binding builds the layer
detailswith one string object per distinct key or value text per call (keys interned as before) and frees the native details of a released layer (about 20 000 small allocations for a 100 KiB text) on a background thread behind a bounded queue instead of with the GIL held; evidence span labels share their strings the same way. The result dicts are unchanged. 100 KiB p95 on the VPS (scripts/l1_presidio/latency.py): ark 15.920 → 14.641 ms, ark-native 9.402 → 9.947 ms (native code unchanged, machine noise); A/B of the binding against main under the same load 17.6–18.0 → 13.2–13.9 ms. M4 not yet met in Python; next pass H06-4. Detection change: none. - L1 latency loop, pass 2 (H06-2). For texts from 32 KiB on the whole per-rule work runs on the
rayon pool: gates, searches, validation, scoring and the
matched_rulesentries of each rule (NativeRegexDetector::rule_findings), joined in rule order, so results are unchanged (PII and DLP). The Python binding builds the layerdetails/thresholdsas Python objects instead of a JSON text round trip (PyLayerResult.details_json/thresholds_jsonare replaced bydetails/thresholds), andPyEvaluationResult.evidence_spansreturns the span dicts of the public result (PyEvidenceSpanis gone); the dicts ofSecurityGateway.scan_categoryand friends are unchanged. 100 KiB p95 on the VPS (scripts/l1_presidio/latency.py): ark 24.651 → 15.920 ms, ark-native 15.324 → 9.402 ms (M4 met native, not yet in Python; next pass H06-3). Detection change: none. - L1 latency loop, pass 1 (H06). Rules of texts from 32 KiB on (
PARALLEL_RULES_MIN_BYTES) are gated and searched in parallel on the rayon pool (NativeRegexDetector::rule_matches); each rule scans the whole text on its own lazy DFA, about 0.18 ms per 100 KiB, which made up about 14.5 of the 24 ms of a 100 KiB PII scan. Validation, scoring and overlap resolution stay sequential in rule order, so results are unchanged (PII and DLP).MatchText::lowercopies and lowercases the ASCII runs of non-ASCII text in place instead of lowercasing every character on its own (100 KiB DE lowercase view and anchors 2.3 → 1.0 ms). 100 KiB p95 on the VPS (scripts/l1_presidio/latency.py): ark 35.898 → 24.651 ms, ark-native 28.441 → 15.324 ms; M4 not yet met, the remaining hotspots are inscoreboard/latency.mdand pass H06-2. Detection change: none. - L1 PII latency pass 1 (H03). A Unicode word boundary (
\b) sends the regex lazy DFA to the NFA at the first non-ASCII byte, so on German prose (ä,ß) most PII rules ran on the slow engine: 100 KiB DE took about four times as long as EN. Each PII rule with a Unicode word boundary now searches its pattern without word boundaries on the DFA and probes each candidate start anchored with its own regex; the matches equalcaptures_iter. Every PII rule is now behind an anchor gate or a prefilter: rules without a lexical anchor or Unicode word boundary (bare checksum numbers, e-mail) get a proven value run (ValueClass::DigitorAlphanumeric, the longest run is measured once per text), andevery_rule_has_an_anchor_gate_or_prefilterfails for an ungated rule. The context-boundpii_fiscal_code_itfolds case in ASCII only ((?i-u)), so it no longer matches a code spelled withſor the Kelvin sign. The gateway checks the network-diagnostic markers once per text instead of lowercasing the whole text for every private IP span, moves L1 results to the caller instead of cloning them, and movesmatched_rulesinto the details instead of deep-copying them. No detection change: goldens, the rule fixture and the scoreboard are unchanged. 100 KiB p95 on the VPS inresearch/l1-pii/scoreboard/latency.md. - LLM and Google provider formats (F-LLM, F06 + T02).
dlp_openai_key(sk-proj-, now alsosk-admin-) anddlp_openai_svc(sk-svcacct-) require two halves of 74 or 58 base64url characters around theT3BlbkFJmarker,dlp_openai_legacysk-, 20 base62 characters, the marker and 20 more (keywordT3BlbkFJinstead ofsk-),dlp_anthropic_keysk-ant-api03-orsk-ant-admin01-, 93 base64url characters andAA(gitleaks openai-api-key, anthropic-api-key, anthropic-admin-api-key). One shared validator (is_llm_api_key) checks that every random part holds upper- and lower-case letters, so typed fills of the shape are no findings.dlp_huggingfacetakeshf_andapi_org_with exactly 34 letters,dlp_groq_keygsk_and 52,dlp_replicater8_and 37 base62 characters,dlp_gcp_client_secretGOCSPX-and 28,dlp_google_oauth_tokenya29.and at least 30 characters up to 2048 bytes with both cases instead of open{20,}/{28,}/{48,}runs; withdlp_google_api_key(AIzaand 35) these prefix rules carry their body length, so generic rules skip prefixed values of another length. Format sources inresearch/dlp/rule_sources.yaml, entity map backlogF-LLM. Newrust/tests/dlp_llm_google_tokens.rs, eleven lookalike generators (wrong length, marker or alphabet), generators for OpenAI admin keys and HuggingFace organization tokens, 34 newdlp_l1rows (DE, EN). Detection change (all modes: OpenAI/Anthropic/HuggingFace/Groq/Replicate/Google values of another length, without the OpenAI marker or with a single-case body are no longer reported by their provider rule; HuggingFaceapi_org_tokens and OpenAIsk-admin-keys are new findings). Regexes stay linear with bounded repeats; the legacy OpenAI rule runs only whereT3BlbkFJoccurs. - Crypto wallet addresses (A02). New label
CRYPTOin the new moduleark-core/src/detectors/pii/crypto.rs:pii_crypto_btc(Bitcoin P2PKH1…and P2SH3…in Base58Check with the version byte0x00/0x05over a 20-byte hash, segwitbc1…with witness version 0 in Bech32 and a 20- or 32-byte program or versions 1–16 in Bech32m, BIP 173/350),pii_crypto_ltc(LitecoinL…/M…, versions0x30/0x32, segwitltc1…) andpii_crypto_eth(Ethereum0xand forty hex digits in mixed case with the EIP-55 checksum over Keccak-256) report the bare value in every mode: four bytes of double SHA-256 or a 30-bit BCH code pass a random string about once in a billion.pii_crypto_eth_unchecked(lower case, no checksum) reports only with a wallet term (new lexicon entrypii.crypto_wallet, EN and DE) near it or as its key. Keccak-256 joins the shared validator library (validators::keccak256).pii.crypto_walletmoves frompresidio_onlytoshared(Presidio'sCRYPTOcovers Bitcoin only); M3 ARK 82 → 83, shared 32 → - New
rust/tests/pii_crypto_wallets.rs, 22 newpii_l1rows (DE, EN), rule fixture entries, Python ports inscoreboard.py. Detection change (strict,balanced,recall: newCRYPTOfindings). Four new regexes, linear and bounded (fixed character classes and bounded repeats between word boundaries); validators run only on matches. - Mobile device and SIM identifiers (B-DEVID). Four new labels in the new module
ark-core/src/detectors/pii/device.rs:IMEI(fifteen digits, plain or grouped 2-6-6-1 with spaces or hyphens, Luhn check digit, not one repeated digit),MEID(fourteen upper-case hex digits led by a regional codeA0–FFand a check digit computed by Luhn in base 16),ICCID(19 or 20 digits led by the telecom prefix89, plain or in blocks of four, Luhn check digit) andIMSI(fifteen digits led by a mobile country code2xx–7xx, format only).pii_imei,pii_meid,pii_iccidandpii_imsireport the value in every mode only when a term of the label (new lexicon entries with EN and DE terms) stands near it or is its key, also as a CSV cell: one bare fifteen-digit number in ten passes Luhn, so none is reported bare.validators::luhn_checksumtakes the base (10 or 16). Four newark_onlyids (pii.imei,pii.meid,pii.iccid,pii.imsi); M3 ARK 75 → 78. Newrust/tests/pii_device_ids.rs, 80 newpii_l1rows (DE, EN), rule fixture entries, Python ports inscoreboard.py. Detection change (strict,balanced,recall: the four labels with a term or key). Four new regexes, linear and bounded (fixed-length character classes with word boundaries); validators run only on matches, the context check only on validated values. -
Vehicle, securities, legal entity and pharmaceutical identifiers (B-FIN). Four new labels:
VIN(seventeen characters withoutI,OandQ, at least one letter, the last four digits;pii_vinchecks the North American check digit in the ninth place, weighted mod 11 withXfor 10,pii_vin_formataccepts the European form without a check digit),ISIN(two capital letters, nine alphanumerics, a Luhn digit over the letters read as 10–35),LEI(eighteen alphanumerics, two digits, ISO 7064 MOD 97-10;validators::mod97andleishare one remainder function) andDE_PZN(Pharmazentralnummer, eight digits or the seven of before 2013, weighted 1–7 mod 11). All five rules report the value in every mode only when a term of the label (new lexicon entries with EN and DE terms) stands near it or is its key, also as a CSV cell: every check passes one order, serial or article number in ten or eleven, so none is reported bare. The short termPZNfollowed by blanks only (Pantoprazol 40 mg, PZN 43230555, the medication line of the E05 doctor's letter, a hard negative there) stays silent;PZN:,PZN-, the keypznandPharmazentralnummerreport. The ICD-10 part of C07 is dropped. Four newark_onlyids (pii.vin,pii.isin,pii.lei,pii.de.pzn); M3 ARK 78 → 82. Newrust/tests/pii_fin_identifiers.rs, 80 newpii_l1rows (DE, EN), rule fixture entries, Python ports inscoreboard.py. Detection change (all modes: the four labels with a term or key). Four new regexes, linear and bounded (fixed-length character classes with word boundaries); validators run only on matches. -
German format identifiers (B-DEFMT).
validators::de_tax_numberchecks the Steuernummer against the groups the Länder print instead of any 10 to 13 digits (F13):FF/BBB/UUUUP(BE, HB, HH, NI, RP, SH),FFBBB/UUUUP(BW),FFF/BBB/UUUUP(BY, BB, HE, MV, SL, SN, ST, TH),FFF/BBBB/UUUP(NW), with any leading digit (test and sample values such as987/654/32109use any), grouped by/, spaces or-or ungrouped, and the 13-digit ELSTER form: a Land prefix of the Bundesfinanzamtsnummer (21–24, 26–28, 30–32, 40, 41, 51–53, 91–92), a0, district and serial, or the two-digit Land code 01–16 in front of eleven digits (0181508150000, the presidio-analyzer notation). Twelve digits, other groupings and ELSTER values without a Land prefix (0081508150001) are rejected; the ELSTER form stays field-bound. New labelCOMMERCIAL_REGISTER_DE(B03):HRA/HRB, one to six digits without a leading zero, optional court suffixB,FL,HL,KIorPI.pii_commercial_register_dereports it in every mode with a register or local court term (new lexicon entrypii.commercial_register, EN and DE) near it or as its key;pii_commercial_register_de_context_freereports the bare value inbalanced/recallat 0.50 unless a field names another value.pii.de.commercial_registerbecomessharedand counts for M2. Newrust/tests/pii_de_format_ids.rs, newpii_l1rows (DE, EN), the 12-digit golden1234/567/89012is now a hard negative. Detection change (strict: fewer tax number forms after the field name, the register number with a term or key;balanced/recall: also the bare register number). One new regex, linear and bounded; the validator is a single pass without allocation. -
US medical licence numbers (A06). Two new labels:
MEDICAL_LICENSE(DEA registration number: a registrant type letter A–H, J–M, P, R–U or X, the initial of the last name or9, seven digits, the last the units digit ofd1 + d3 + d5 + 2 × (d2 + d4 + d6)) andUS_NPI(National Provider Identifier: ten digits, first 1 or 2, plain or grouped 4-3-3 with spaces or hyphens, no repeated digit before the check digit, Luhn check digit behind the prefix80840).pii_dea_number_usandpii_npi_usreport the value in every mode when a term of the label (new lexicon entries with EN and DE terms) stands near it or is its key, also as a CSV cell;pii_npi_us_context_freereports the bare grouped NPI inbalanced/recallat 0.55 (K05). Two letters and seven digits pass the DEA digit one time in ten and ten plain digits the NPI digit one time in ten, so neither is reported bare. Thepresidio_onlyidspii.us.medical_licenseandpii.us.npibecomeshared; M3 ARK 73 → 75, shared 30 → 32. Newrust/tests/pii_us_medical.rs, 43 newpii_l1rows (DE, EN), rule fixture entries, Python ports inscoreboard.py. Detection change (strict: the two labels with a term or key;balanced/recall: also the bare grouped NPI). Three new regexes, linear and bounded (fixed-length character classes with word boundaries); validators run only on matches. - CLI credentials (G03). New rule
dlp_cli_password(CREDENTIAL, subtypepassword, structural credential tier) reports the password ofcurl -u/--user user:…, the MySQL/MariaDB clients (-p…attached,--password=…), a shellPGPASSWORD=…,sshpass -p,docker/podman login -p/--password,htpasswdin batch mode (-b, the last argument), PowerShellConvertTo-SecureString "…" -AsPlainText(also piped) andnet use \\server\share … /user:. Prompts (curl -u user,mysql -p db,net use … *),--password-stdin,sshpass -e/-f, shell, CI and template references and stand-ins are no findings. The entitycredential.cli_password(gitleakscurl-auth-user) moves frombaseline_onlytoshared; its synthetic carriers and generators belong to the rule inrules.json. Newrust/tests/dlp_cli_passwords.rs, 28 newdlp_l1rows (DE, EN), rule fixture entry. Detection change (every mode): these command-line passwords are newCREDENTIALfindings; aPGPASSWORD=…value thatdlp_password_assignmentreported before now reportsdlp_cli_password. One new regex with literal keywords (prefilter:curl,mysql,mariadb,pgpassword,sshpass,docker,podman,nerdctl,buildah,skopeo,htpasswd,convertto-securestring,net use), bounded gaps (≤ 300 characters within a line) and bounded values. -
Structure-aware config keys (G02). In a config document
dlp_generic_credential_assignmentalso binds the keyscredentials,privateKey/private_key,secret_key,signing-key,passphraseand the DE variantsZugangsdaten,Anmeldedaten,privater/geheimer Schlüssel(also compound and camelCase:jwt.privateKey,LAGER_SIGNING_KEY), through the K05–K05C extraction of YAML, JSON, TOML, HCL, XML, INI/properties and .env. A text is a config document when it is a JSON object/array or JSON Lines, an XML document without HTML markup, or when every line is a config line, comment, section, HCL block head or closer, YAML list item or block scalar text (config_keys::is_config_document); program source (declarations, calls, statements ending in;/,,class X {,const x = {) never is, so object literals, kwargs, struct fields and test fixtures keep the base behaviour, and the rule regex is unchanged. Values under these keys pass the existing value gate and shape check and must not name a key (value_shape::is_key_reference): no key, certificate or config file name (./certs/signing.pem,classpath:keys/private.pem), no GPG key ID or upper-case fingerprint (signingkey = …in.gitconfig), AWS/GCP KMS key name, identifier or text with blanks. Newrust/tests/dlp_config_document_keys.rs, 25 newdlp_l1rows (DE, EN; 13 hard negatives, 9 from program source), 6 rule fixture hard negatives. Detection change (every mode): values under these keys in config documents are newCREDENTIALfindings; on dev benign code, benign docs, the external corpora and the synthetic carriers every finding is byte-identical to the base (0 newdlp_generic_credential_assignmenthits). The document check runs once per text and family of formats, only when one of these keys holds a config value. -
Indian identifiers (B-IN). Four new labels:
IN_AADHAAR(twelve digits, first digit 2–9, plain or grouped 4-4-4 with spaces, hyphens or colons, no palindrome, Verhoeff check digit),IN_GSTIN(state code 01–38/97/99, the holder's PAN, entity number,Zand a check character weighted 1-2 mod 36),IN_PAN(five letters with the holder type as the fourth, four digits other than0000, a letter; format check) andIN_VOTER(EPIC, three letters and seven digits; format check).pii_aadhaar_in,pii_gstin_in,pii_pan_inandpii_voter_id_inreport the value in every mode when a term of the label (new lexicon entries with EN and DE terms) stands near it or is its key, also as a CSV cell;pii_aadhaar_in_context_freeandpii_gstin_in_context_freereport the bare grouped Aadhaar number and the bare GSTIN inbalanced/recallat 0.55 (K05). The fourpresidio_onlyidspii.in.*becomeshared; M3 ARK 71 → 73, shared 29 → 30. The Python ports inscoreboard.pyreuse the Verhoeff and GSTIN helpers ofsynth_goldset.py. Newrust/tests/pii_in.rs, 86 newpii_l1rows (DE, EN), rule fixture entries. Detection change (strict: the four labels with a term or key;balanced/recall: also the bare grouped Aadhaar number and GSTIN). Six new regexes, linear and bounded (fixed-length character classes with word boundaries); validators run only on matches. -
Payment fields (Q-FIN, quality floor F1 ≥ 80). A card security code whose field names the card (
Credit Card Security Code: 279,Kreditkarte, Sicherheitscode: 123,Creditcardveiligheidscode: 713,Code de sécurité de la carte de crédit : 965) belongs to a card without the card number in the text;security codeis a CVV field name, and a bareCVV:/Security code:still needs a reported card or a CVV column. SWIFT/BIC accepts combined field names (Swift/BIC-Code:,SWIFT-BIC:,swift_bic_code,<swiftBIC>), a note before the colon (Swift BIC Code (for international transactions):,… for Bank Transfer:) and prose assignments (BIC COBADEFFXXX,SWIFT BIC code "X",Als BIC ist X,code SWIFT est X). The account rule bindsKonto-Nr.:,Als Kontonummer ist …andBank account on file is …, andPermanent Account Number(the Indian PAN) is no longer an account. Detection change (every mode): these values are newCREDITCARD_CVV/SWIFT_CODE/FINANCIAL_ACCOUNT_NUMBERfindings, three Indian PAN values lose their account label; no validator changes. Dev, exact F1, balanced (strict equal): CVV neutral 0.0 → 63.4, SWIFT/BIC neutral 38.5 → 73.4 and ark_authored 36.9 → 100.0, account number ark_authored 78.9 → 100.0; no new FP on hard negatives, no other entity changes. The entities that stay below 80 (card number, CVV and account number neutral, account number presidio_favored, CVV and card expiry ark_authored) are recorded decisions inresearch/l1-pii/README.md(Stand Q-FIN). The new field-name alternatives sit inside the existing literal-anchored regexes; the card-term check runs only on CVV spans without a card. - Driver's licence and passport numbers (Q-ID, quality floor F1 ≥ 80). A driver's licence field
name may be followed by a note in parentheses without a digit (
Führerscheinnummer (falls relevant): 3330234369,licencia de conducir (opcional): G37722093), and the value may open a parenthesis (la licencia de conducir (M45114072));travel document,Reisedokumentand the Korean여권are passport terms of thepii.passportlexicon, so the US passport letter form binds to them (my travel document is A12803456). Detection change (every mode): these values are newDRIVER_LICENSE_NUMBER/PASSPORT_NUMBERfindings; no validator changes. Dev, exact F1, balanced: driver's licence neutral 37.1 → 40.3, passport presidio_favored 47.4 → 55.0 (strict 18.8 → 29.4), no new FP, Gretel and ark_authored unchanged. The entities that stay below 80 (driver's licence neutral, passport and identity card presidio_favored, UK NINO 66.7 from a gold span with a leading space) are recorded decisions inresearch/l1-pii/README.md(Stand Q-ID). The note group is a bounded character class inside the literal-anchored field-name regex; the lexicon gains four terms. - Passport and identity card numbers by field name (B-PASS: T24, A05, B08, T10). New rule
pii_passport_number_fieldreports six to nine upper-case letters and digits, at least five of them digits, right after an explicit passport field name in English, German, Spanish, French, Italian or Dutch (Passport No. X45929398,Reisepass (D5832368),Mi pasaporte es AAA482913,le passeport numéro 07RY12580,passaporto n. M0572847) in every mode (validators::passport_number_field, format only): up to three leading letters and digits or the French07RY12580; one letter and exactly six digits (A482913, the holdout HN-FP of run 20261022T171514Z) or two letters and six digits (AB123456) is no passport form, German document forms stay with the German rules (in strict as before), one repeated digit and seven ascending digits (12345678) are placeholders. With a document term near the value or as its key, in every mode: the US passport letter form, one letter and eight digits (pii_passport_number_us; nine digits staypii_passport_number_uk), the Austrian and Swiss passport and identity card, one letter and seven digits (pii_passport_number_at_ch,pii_identity_card_number_at_ch), the Italian carta d'identità (CIECA48213EF,pii_identity_card_number_it; Italian terms in thepii.identity_cardlexicon) and the Spanish pasaporte, three letters and six digits (pii_passport_number_es). Newrust/tests/pii_passport.rs, 117 newpii_l1rows (DE, EN, ES, FR, IT, NL), one rule fixture entry per rule. Detection change (every mode): passport and identity card numbers in these forms are newPASSPORT_NUMBER/IDENTITY_CARD_NUMBERfindings; no existing rule or validator changes. Dev scoreboard vs. 227c840 (PASS in strict and balanced, no new hard-negative FP):entity.passport_numberexact F1 +12.2 pp strict, +11.4 pp balanced (exact true positives neutral 37 → 51, presidio_favored 6 → 9 balanced);pii.identity_card_number+25.5 / +18.7 pp; L1 F1 +0.4 pp in both modes. The new rules are literal-anchored (field names) or context-bound bounded regexes without per-match allocation. - DLP Q-DLP-PW: chosen passwords whose letter runs, dots and
~read like a member access (DB_PASSWORD=wPF.DZs~…,db_password = "o.Bbst….jdJ44") are values ofdlp_password_assignmentagain. The member-access check of the F-GEN1 likeness (likeness::MEMBER_ACCESS, shared by the assignment, CSRF and connection-password gates) no longer ends a chain at~or at a.before a digit or symbol, which code cannot do; member access ending at the value end,;,,,),[,(or an operator stays code. New generatorsdotted_tilde_passwordanddotted_digit_password, 20dlp_l1rows (10 positives, 10 hard negatives, DE and EN), rule fixture and overlap tests. Detection change (every mode): such values are newCREDENTIALfindings; under a password variable the span moves fromdlp_env_var_secrettodlp_password_assignment. Dev synthetic carriers:credential.password_assignmentexact recall 39/40 → 40/40, no other row changes; benign code and benign docs unchanged. The regex is the same linear pattern with two more excluded characters (no latency effect). - IBAN per the SWIFT IBAN registry (B-IBAN: T05, T21, F03). New validator
ibanforpii_iban_deandpii_iban_generic: a registry country (89), exactly its registry length, two check digits, letters and digits only, and MOD-97 (mod97itself is unchanged); the scoreboard port isiban_registry,checksums.arklistsibaninstead ofmod97(no M3 change).pii_iban_genericreads every registry country compact or in blocks of four with letters (GB82 WEST 1234 5698 7654 32,IT60 X054 2811 …), all upper or all lower case (nl91 abna 0417 1643 00);pii_iban_dealso takesde. A grouped match that runs into the next word or block (BE68 5390 0754 7034 und) is cut at the registry length of its country before validation. Newrust/tests/pii_iban.rs(one vector per registry country, wrong lengths with passing MOD-97, creditor IDs, grouped and lowercase detection), 25 newpii_l1rows (13 positives, 12 hard negatives, DE and EN), the two grouped AT/GB known gaps are positives now, rule fixture with creditor-ID hard negatives. Detection change (every mode,strictincluded as the task requires): grouped and lowercase IBANs of all registry countries are newIBANfindings; values with a non-registry length, among them SEPA creditor IDs such asDE30ZZZ56545348553that pass MOD-97, are no longer reported. Dev scoreboard vs. 6c680c7 (PASS in strict and balanced, no new hard-negative FP):pii.ibanexact F1 75.0 → 97.8 in both modes; L1 F1 strict +1.0 pp, balanced +0.9 pp. The rule regex is linear and bounded; the cut is one pass over the match, only for over-long grouped matches. - Detection change (every mode): after a match is cut at its registry length, the scan of
pii_iban_genericresumes at the cut instead of the end of the over-long match, so an IBAN right after another one (BE68539007547034 BE68539007547034, two grouped IBANs in a row) is reported as well instead of being swallowed (Codex review on #219). - DLP G-PROSE (G06): credentials handed over in chats, tickets and mails. New rules
dlp_prose_password(CREDENTIAL, subtypepassword, entitycredential.password_assignment) anddlp_prose_secret(CREDENTIAL,generic_secret, entitycredential.secret_assignment), generic overlap tier, format sourceark. They report the value after a password noun (Passwort, Kennwort, password, passphrase) or a secret/token/API-key noun when it follows a:on the noun's line or the next one and stands at the end of its line or alone up to two lines further („Wie lautet das Datenbank-Passwort?“ and the reply, „here is the client secret:“ and the value below, „(Datenbank-Passwort): …“);Zugangsdaten/credentialsonly asuser / password. The value must look chosen or issued (likeness::is_prose_value: item-like character classes, a letter, no date, e-mail address or numbered identifier, letters only with random case changes), and the placeholder, example and reference checks of the generic rules apply. A value right after the noun stays with the assignment rules; a hash or other value noun between noun and value, a field label of its own on a later line and a provider token prefix (npm_…) reject it. New carrier kindhandover(chat/ticket/mail families only),rules.jsonprobes, 24dlp_l1rows (12 positives, 12 hard negatives incl. encyclopedia and law prose, DE and EN), fixture entries,rust/tests/dlp_prose_credentials.rsand likeness unit tests. Detection change: values handed over in prose are newCREDENTIALfindings. Dev chat/ticket/mail carriers, default secret groups: 24 of 30 values found (0 before; the 6 left are password hashes, G-HASH), no new hard-negative FP, benign docs 0 new spans, benign code 0 new spans. Both regexes are bounded (noun, at most 160 + 80 characters to the:, a value of at most 100 characters) and run only on their prefilter keywords; 40 benign-code documents of 100 KiB with the two rules gated on vs. off: p50 8.0 vs. 7.8 ms, p95 10.9 vs. 11.9 ms (same build, run noise, no measurable change). PIN/TAN/PUK and Wi-Fi keys (G07) are not part of this change. - Detection change (every mode): a value right after a noun is left to the assignment rules
only when one of them reads that noun (
Passwort,Kennwort,password,passwd,secret,token); afterpassphrase,Passwörter,Kennwörter,passwords(passphrase:and the value below)dlp_prose_passwordreports it instead of no rule (Codex review on #220). - Regional phone numbers via the
phonenumbercrate (F01). New rulepii_phone_regional(PHONE,balanced/recallonly, after the other phone rules, which keep their spans): a grouped national number without a country prefix (Mobile: 416 60 039,call me on 03.93.92.16.85,0688 843 79 56,(64) 3591-3246) with seven to twelve digits, not a date, supported by a phone term of the lexicon or a structural key, and valid in one of 24 numbering plans (DE, AT, CH, FR, BE, NL, LU, IT, ES, PT, IE, GB, DK, NO, SE, FI, PL, CZ, US, CA, BR, IL, IN, JP) per the libphonenumber metadata ofphonenumber0.3.10 (Apache-2.0; both cargo-deny license checks pass). The plan is checked only after the context gate, so digit runs without a phone term are never parsed. Validatorregional_phoneis format only (checksums.ark, no M3 change). T01 placeholder numbers (NANP 555-01xx,+1 000-000-0000) stay positive. Newrust/tests/pii_phone_regional.rs, 21 newpii_l1rows (11 positives, 10 hard negatives, DE and EN,balanced), rule fixture entry with hard negatives. Detection change (balanced/recallonly;strictoutput is byte-identical): the national numbers above are newPHONEfindings. Dev scoreboard vs. b455201 (PASS in strict and balanced, no new hard-negative FP), exact F1 balanced: phonepresidio_favored80.5 → 91.8 (Presidio tuned 65.1, precision 100.0),neutral81.9 → 82.1 (tuned 36.7),ark_authoredunchanged; L1 F1 balancedpresidio_favored+1.5 pp. The rule regex is linear and bounded.latency.py compare --engine ark-native100 KiB p95 175.5 → 179.2 ms (+2.1 %), PASS.
Added¶
-
Synthetic DLP gold for the 13 payment, observability and llm entities without an ARK rule and without gold (GOLD-D5):
llm.cohere.api_key,llm.perplexity.api_key,llm.privateai.api_key,observability.sumologic.access_token,payment.square.access_token,payment.shopify.token,payment.coinbase.access_token,payment.plaid.secret,payment.gocardless.api_token,payment.flutterwave.secret,payment.finicity.secret,payment.crypto_exchange.secret,payment.freemius.secret_key(packages P07, P08, P09).baseline_entitiesindlp_synth/rules.jsongains 24 generators in the format of each entity's gitleaks rule (Perplexitypplx-, SquareEAAAandsq0atp-, Shopifyshpat_/shpca_/shppa_/shpss_, Plaidaccess-production-+ UUID, GoCardlesslive_, FlutterwaveFLWSECK_TEST-secret and encryption key, Kraken base64 private key, KuCoin, Bittrex, Freemiussk_and the keyed shapes of the other rules); KuCoin and Bittrex keys sit inKUCOIN_…/BITTREX_…variables and the Freemius key in a PHP'secret_key' => '…'entry (profile wrapper). Each entity has one lookalike with a wrong length (Flutterwave: no dash after the prefix). Per split and entity the carriers give 44 positives (DE and EN) and 220 hard negatives.entity_map.yamlmaps the TruffleHog detectors that report these formats (SumoLogicKey, Square, GoCardless;check-entity-mappasses on the pinned binaries). The other entities of the three families already had carrier profiles. No detection change: evaluation data only, the Rust detector is untouched (no latency effect). Dev rows (exact, untyped value spans): ARK has no provider rule, its generic rules find 9 of the 13 (F1 66.7 to 97.7; 0 on Sumo Logic, Finicity, Flutterwave, Freemius) and report 42 hard-negative spans on the KuCoin/Bittrex and Plaid test-key, example and placeholder rows; gitleaks 22.7 to 98.9, TruffleHog 0 except Sumo Logic 88.9, GoCardless 91.3, Square 61.1, detect-secrets 0 to 69.9. Frozen corpora change only when the operator re-freezes them (T19). -
Synthetic DLP gold for the 25 saas entities without an ARK rule and without gold (GOLD-D1):
saas.atlassian.api_token,saas.notion.api_token,saas.linear.secret,saas.airtable.token,saas.asana.client_secret,saas.hubspot.api_key,saas.zendesk.secret_key,saas.intercom.api_key,saas.postman.api_token,saas.mapbox.secret_token,saas.launchdarkly.access_token,saas.typeform.api_token,saas.contentful.delivery_token,saas.dropbox.api_token,saas.adobe.client_secret,saas.facebook.secret,saas.twitter.secret,saas.linkedin.client_secret,saas.looker.client_secret,saas.twitch.api_token,saas.etsy.access_token,saas.freshbooks.access_token,saas.squarespace.access_token,saas.misc.api_token,saas.sidekiq.credentials(package P10).baseline_entitiesindlp_synth/rules.jsongains 41 generators in the format of each entity's gitleaks rule (AtlassianATATT3and legacy tokens, Notionntn_, Linearlin_api_, Airtablepat….<64 hex>, PostmanPMAK-, Mapboxpk., LaunchDarklyapi-/sdk-+ UUID, Dropboxsl.and long-lived tokens, Adobep8e-, Facebook<app id>|<secret>andEAACpage tokens, the five Twitter shapes, 42 intra, MaxMind_mmk, ReadMerdme_, Frame.iofio-u-and the keyed shapes of the other rules); Sidekiq gem credentials sit inBUNDLE_ENTERPRISE__CONTRIBSYS__COM=…orhttps://…@enterprise.contribsys.com/. Each entity has one lookalike with a wrong length. Per split and entity the carriers give 44 positives (DE and EN) and 220 hard negatives.entity_map.yamlmaps the TruffleHog detectors that report these formats (Atlassian, JiraToken, LinearAPI, AirtablePersonalAccessToken, HubSpotApiKey, Postman, LaunchDarkly, Typeform, Dropbox, FacebookOAuth, TwitchAccessToken, Squarespace, FrameIO, MaxMindLicense, ReadMe;check-entity-mappasses on the pinned binaries). No detection change: evaluation data only, the Rust detector is untouched (no latency effect). Dev rows: ARK exact F1 0 on all 25 (no rule yet), gitleaks 44.6 to 97.7, TruffleHog 0 to 96.5, detect-secrets 0. Frozen corpora change only when the operator re-freezes them (T19). -
Polish PESEL and REGON with check digits, bare NIP (A13). Two new labels:
PL_PESEL(eleven digitsYYMMDDZZZXC, a birth date from 1900 to 2100 with the century in the month, check digit weighted 1-3-7-9 mod 10) andPL_REGON(nine digits weighted 8-9-2-3-4-5-6-7 mod 11, or fourteen whose first nine are a REGON of their own, weighted 2-4-8-5-0-9-7-3-6-1-2-4-8 mod 11).pii_pesel_plandpii_regon_plreport the value in every mode when a term of the label (EN, DE, PL) stands near it or is its key, also as a CSV cell; bare runs of nine, eleven or fourteen digits are never reported. The NIP reuses the B16 validator of the Polish VAT ID: the newpii_vat_id_pl_proximityreports a bare NIP (ten digits, alsoNNN-NNN-NN-NNorNNN-NN-NN-NNN) underVAT_ID_PLwhen a NIP term (new lexiconpii.pl_nip) decides or is its key, scored modes only (a generic VAT term near ten valid digits is as often the spaced VAT ID of another country,SK 928 762 01 99, or another number such as an NPI), like the bare Partita IVA and CIF, so it is no second M3 unit; the digits afterPL/PL-stay the body of the VAT ID. Thepresidio_onlyidpii.pl.peselbecomesshared,pii.pl.regonis new andark_only; M3 ARK 69 → 71, shared 28 → 29. Newrust/tests/pii_pl.rs, 68 newpii_l1rows (DE, EN, PL), rule fixture entries. Detection change (strict: PESEL and REGON with a term or key;balanced/recall: also the bare NIP with a term). The rule regexes are linear and bounded. - Detection change (
balanced/recall): a bare NIP in a cell under a NIP key is reported also next to a numeric cell (nip,employees\n5260250995,42), like PESEL and REGON; in prose the digits still have to stand alone. - Spanish NIF/NIE and the Italian codice fiscale with check letters, bare CIF (B-ESIT: A11, A12).
Three new labels:
ES_NIF(DNI/NIF, eight digits or seven of an older DNI and the control letter mod 23),ES_NIE(X/Y/Zas 0/1/2, seven digits, the same control letter) andIT_FISCAL_CODE(sixteen characters, month letter, day plus 40 for women, omocodia letters, check letter by the odd/even tables mod 26).pii_nif_es,pii_nie_esandpii_fiscal_code_itreport the value in every mode, in any case, when a term of the label (EN, DE, ES/IT) stands near it or is its key; inbalanced/recallthe uppercase values are also reported bare at 0.55 (K05). A NIF or NIE right afterES/ES-(a NIF also afterCY/CY-) stays the body of a VAT ID. The CIF and the Partita IVA reuse the B01 VAT validators: the newpii_vat_id_es_proximityreports a bare CIF underVAT_ID_ESwith a VAT or CIF term (scored modes only, like the bare Partita IVA ofpii_vat_id_it_proximity), so neither is a second M3 unit. The threepresidio_onlyidspii.es.nif,pii.es.nieandpii.it.fiscal_codebecomeshared; M3 ARK 66 → 69. Presidio also reports codici fiscali with a wrong check letter; ARK does not. Newrust/tests/pii_es_it.rs, 86 newpii_l1rows (DE, EN, ES, IT), rule fixture entries. Detection change (strict: the three labels with a term or key;balanced/recall: also the bare uppercase values and the bare CIF with a term). The rule regexes are linear and bounded. - Korean resident, foreigner and business registration numbers with check digits (B-KR: A14 and the
checksum part of T12). Three new labels:
KR_RRN(주민등록번호YYMMDD-GHHSSSC: birth date, sex and century digit 1–4, region code up to 95, check digit(11 − weighted sum mod 11) mod 10),KR_FRN(외국인등록번호, the same form with 5–8 and(13 − …) mod 10) andKR_BRN(사업자등록번호NNN-NN-NNNNN, weighted 1-3-7-1-3-7-1-3-5 mod 10).pii_rrn_kr,pii_frn_krandpii_brn_krreport the value in every mode when a term of the label (EN, DE, Korean) stands near it or is its key, also as a CSV cell next to numeric cells; inbalanced/recallthe hyphenated forms are also reported bare at 0.55 (K05). Thirteen and ten bare digits always need a term. RRNs issued since October 2020 carry no check digit and are not reported. The threepresidio_onlyidspii.kr.rrn,pii.kr.frnandpii.kr.brnbecomeshared; M3 ARK 63 → 66, shared 22 → 25. The KR driving licence and passport (format and context only) stay out. Newrust/tests/pii_kr.rs, 76 newpii_l1rows (DE, EN, KO), rule fixture entries. Detection change (strict: the three labels with a term or key, including theCREDITCARDhard negatives that are valid RRNs, now annotated asKR_RRN;balanced/recall: also the bare hyphenated forms). The rule regexes are linear and bounded.
Detection changes¶
-
DLP L1 (T18, all modes): the T06 example check of the generic rules (
dlp_env_var_secret,dlp_generic_credential_assignmentand the other generic assignments) treats a value as machine-issued not only when it is 32 or more hex digits, but also when it holds a hex, digit or base62 run of at least 24 characters next to a shorter prefix or suffix (SK…,dapi…,…-us10,shippo_live_…,sqp_…,CLOJARS_…, dash-separated hex groups;credentials::machine_issued_run), and applies the fixed-format repetition check to that run. Documented example and test values of providers without an ARK rule (deadbeef…,1234567890…fills) are dropped without example context; hand-typed weak passwords (abcabcabcabc,Passwort123!) and values with a random run stay findings. Detection change: on the GOLD-D2 baseline-entity rows of the current dev carrier generator (seed 20261011, 4,664doc_example/test_keyrows), hard-negative FP of the two generic rules 445 → 10 (the rest are 1Passwordops_base64 blobs without a machine run); positives unchanged (exact TP 4,327 → 4,327), no span gained anywhere. The check runs only on a matched generic value;latency.py bench100 KiB p95 11.18 → 11.11 ms (noise).dlp_l1.jsonlgains 22 rows (10 positives, 12 hard negatives as fill recipes, DE + EN). -
DLP L1 observability tokens (P07, all modes): seventeen new provider rules, each with a format source in
research/dlp/rule_sources.yaml; tokens areSECRET_TOKEN(subtypeaccess_token), keysAPI_KEY(api_key). Prefix rules with a token boundary:dlp_newrelic_user_api_key(NRAK-),dlp_newrelic_insert_key(NRII-),dlp_newrelic_license_key(32 hex digits oreu01xxand 26, suffixFFFFNRAL),dlp_sentry_user_token(sntryu_),dlp_grafana_service_account_token(glsa_, checked against its CRC32, little-endian hex as in Grafana'ssatokengen) anddlp_dynatrace_token(dt0c01,dt0s01–dt0s16). Base64 tokens, decoded and parsed:dlp_sentry_org_token(sntrys_, JSON payload with a numericiat),dlp_grafana_api_key({"k","n","id"}) anddlp_grafana_cloud_token(glc_, object withk). Key-anchored rules (provider name and a credential noun right before the separator):dlp_datadog_api_key,dlp_newrelic_legacy_license_key,dlp_sentry_auth_token,dlp_splunk_hec_token(alsoAuthorization: Splunk),dlp_elastic_api_key(id:api_keyor its base64, alsoAuthorization: ApiKey, decoded and split),dlp_honeycomb_api_key(alsox-honeycomb-teamandOTEL_EXPORTER_OTLP_HEADERS) anddlp_pagerduty_api_key(alsoToken token=with PagerDuty context, since Rails uses the same scheme).dlp_sentry_dsn_secretreports the secret key of a legacy DSN (https://<public>:<secret>@host/<project id>); a DSN with the public key only is public by Sentry's docs and no finding. The generic key-bound rules no longer report a Datadog client token (puband 32 hex digits, public for browser SDKs; newvalue_shapeshapePublicKey). The baseline entities for Datadog, New Relic, Sentry, Grafana and Dynatrace becomeshared, Honeycomb and PagerDuty are newsharedentities (TruffleHog detectors), Sentry DSN secrets, Splunk HEC and Elasticsearch API keys areark_only; Sumo Logic staysbaseline_only. The golden generator gains the checksum kindcrc32_hex_le(Python and Rust). Detection change: these tokens are new findings; a value a generic rule reported before now carries the provider rule. Dev scoreboard vs. 9cb48cc (PASS, 67 checks): every group unchanged (the frozen dev corpora have no observability gold yet), FP/MB on benign code 0.053 → 0.053, hard-negative FP 154 → 154; 100 KiB p95 11.02 → 10.93 ms, 10 MiB repo 7.79 → 7.81 MiB/s. Every new regex is linear and bounded and has literal prefilter keywords.dlp_l1.jsonlgains 66 rows (38 positives, 28 hard negatives, DE + EN); newrust/tests/dlp_observability_tokens.rs. -
DLP L1 CI and hosting tokens (P06, all modes): nineteen new provider rules, each with a format source in
research/dlp/rule_sources.yaml. Prefix rules with a token boundary:dlp_azure_devops_pat(84 alphanumerics with the signatureAZDOat positions 76 to 80, both cases),dlp_circleci_token(CCIPAT_/CCIPRJ_, a base58 UUID and 40 hex digits),dlp_vercel_token(vcp_,vci_,vca_,vcr_,vck_and 56 alphanumerics with both cases),dlp_netlify_token(nfp_),dlp_flyio_token(fo1_),dlp_heroku_api_key(HRKU-AA),dlp_render_api_key(rnd_and 24 to 64 alphanumerics, only withrenderon the value's line or up to five lines above, since Render documents no length),dlp_cloudflare_origin_ca_key(v1.0-) anddlp_digitalocean_token(dop_v1_/doo_v1_/dor_v1_);dlp_flyio_macaroon_token(fm1a_/fm1r_/fm2_and base64) has no token boundary because/and+belong to the token. Key-anchored rules report a value under a key that names the provider and ends in a credential noun right before the separator, tighter than the gitleaks keyword rules:dlp_bitbucket_client_secret(API_KEY, subtypesecret_key),dlp_azure_devops_legacy_pat,dlp_circleci_legacy_token,dlp_travis_token,dlp_vercel_legacy_token,dlp_netlify_legacy_token,dlp_heroku_legacy_api_key,dlp_railway_tokenanddlp_cloudflare_api_key; IDs under provider keys without a credential noun (HEROKU_APP_ID,CIRCLE_SHA1,RAILWAY_PROJECT_ID) stay silent. The baseline entities for Bitbucket, Travis CI, Fly.io, Netlify, Heroku, Cloudflare and DigitalOcean becomeshared, Azure DevOps, CircleCI, Vercel and Railway are newsharedentities (TruffleHog detectors), Render isark_only; Drone CI, OpenShift, Defined Networking, Fastly and SettleMint staybaseline_only. Detection change: these tokens are new findings; a value a generic rule reported before now carries the provider rule. Dev scoreboard vs. 7790d59 (PASS, 67 checks): every group unchanged (the frozen dev corpora predate the GOLD-D3/D4 rows, T19), FP/MB on benign code 0.053 → 0.053, hard-negative FP 154 → 154; 100 KiB p95 13.38 → 12.16 ms, 10 MiB repo 6.05 → 6.12 MiB/s (same host load for both builds). Every new regex is linear and bounded and has literal prefilter keywords (the key-anchored rules start at the provider name).dlp_l1.jsonlgains 74 rows (39 positives, 35 hard negatives, DE + EN); newrust/tests/dlp_hosting_tokens.rs.
[0.1.14] - 2026-10-28¶
Added¶
- Standalone
patronus-ark-corePython package, initially published as 0.1.14, with local native scanning and optional Wolf Defender through the shared Core. - Shared
WolfExecutionOptionsfor configurable total window tokens, content overlap, and actual ONNX window batching. Existing constructors retain 256 total tokens, no overlap, and batch size 1. Core owns streaming windows, masks, per-row logits, cancellation, and Smooth Max; Ark delegates through its execution options. - Move the synchronous ONNX text classifier into Core and preserve Ark's public type through a re-export. Reuse one batch-input assembler for the classifier and Wolf, and remove Ark's redundant pre-tokenization/chunk planning for dedicated Wolf documents.
Changed¶
- Share the initialized native producer registry and secret/PII overlap filtering between Core and Ark, removing Ark's duplicate dispatch. Ark retains scheduling, result policy and aggregation; existing Core, Respan and LiteLLM contracts stay unchanged. Core-owned rule catalogs declare MIT OR Apache-2.0 while retaining upstream source-license provenance.
- Expose optional Wolf startup profiles in Full-Ark Python and HTTP configuration; existing strategy and profile defaults remain unchanged. Warmup uses the actual configured profile. Core Python adds optional typed configuration dictionaries.
- Terminate active eligible Wolf inference on Ark request early exit, and enforce
the existing dedicated-Wolf inference budget after model loading and locking.
Inference timeouts retain degraded-timeout behavior and cannot cause sibling-job
cancellation through a positive fallback. Core deadlines still start when the
caller constructs its
ScanControl. - Validate generic classifier output shapes and finite values before interpreting predictions; preserve existing public methods and add checked decoding helpers.
-
Contact and account fields (Q-CONTACT, quality floor F1 ≥ 80). Field-bound values that the rules missed:
pii_usernamebindsuser=<value>in log and query lines (auth failed user=oeztuerk90;useronly with=), no longer readslogin issuesaslogin is+suesand skips a name after its field (Account Name: John Doe,ACME Inc.; a credential line such asUsername: JSmith Password: …stays). The date-of-birth rules share one field part and add the ISO order (1980-11-26,validators::calendar_date),geb.,born on,DateOfBirth=, the connectivesof/del/el, a no-break space and an example opener (date of birth (e.g., …)); written dates take ordinal days (18th December 1983) and the months of FR, ES, IT and NL (26 de marzo de 1990,1er mars 1990,validators::written_calendar_date).pii_employee_idtakesemployé ID,ist/lautet, a field closing a parenthesis and a short uppercase prefix with digits (PN 201033);pii_employee_id_prefixedalsoEMP821659.pii_customer_idtakesa customer ID of …,the customer with (the) ID …,ist/lautetandKundennummer (z. B. …). New structure carrier (K04): a form printed as a table without edge pipes (two or more linesGeburtsdatum | 21. Februar 1971, key cell without digits) binds every field-bound rule. The date validators still check the calendar (1985-02-30,31 de abril de 1990stay negative); no checksum changes. Email (Gretel leaves 103 of 133 Markdownmailtolinks unannotated), username, customer ID and date of birth stay below 80 onneutralby recorded decision (gold gaps,research/l1-pii/README.md, Stand Q-CONTACT). Newrust/tests/pii_contact_quality.rs, 130 newpii_l1rows (69 positives, 61 hard negatives; DE + EN plus FR/ES/IT/NL), three former known gaps closed, hard negatives in the rule fixture. Detection change (every mode): the forms above are newUSERNAME,DOB,EMPLOYEE_IDandCUSTOMER_IDfindings, values in edge-less key/value tables new findings of their field's rule, andlogin is…and names afterAccount Name:are no longerUSERNAME. Dev scoreboard vs. 94aca75 (every gated bucket up, strict and balanced, no new hard-negative FP), exact F1 balanced: username ark_authored 35.3 → 98.2, neutral 8.8 → 10.3; DOB ark_authored 71.4 → 99.7, neutral 66.7 → 67.0; employee ID neutral 79.0 → 88.2, ark_authored 87.5 → 100.0; customer ID neutral 51.1 → 60.9; phone presidio_favored 80.5 (unchanged); L1 F1 neutral 65.8 → 66.7, ark_authored 54.4 → 59.6 (strict). New regex parts are bounded alternations and fixed digit counts; the table carrier is one pass over the lines that hold a|.latency.py compare --engine ark-native100 KiB p95 161.6 → 167.9 ms (+3.9 %) and 161.6 → 157.7 ms (−2.4 %) in two head runs, both PASS. Golden eval green. -
German IDs on
presidio_favored(Q-DE, quality floor F1 ≥ 80). The bare KVNR rulepii_health_insurance_number_de_context_freenow reads the letter in either case (a123456780; the check digit counts it like the uppercase one), like the bare passport and identity card rules. New scored-only format rulepii_steuernummer_de_context_free: a bare Steuernummer in the slashed groups of the Länder (2/3/5, 3/3/5, 3/4/4:21/815/08150,123/456/78901,133/8150/8159,validators::de_tax_number) inbalanced/recallat 0.50 (0.75 near a tax number term), silent under a field that names another value and when a counter-context term is nearer. The 13-digit ELSTER form stays field-bound. LANR (bare nine digits), Steuer-ID (98765432106, never issued) and DE VAT ID (DE987654321,DE100000001, failed check digit) stay below 80 onpresidio_favoredby recorded decision (research/l1-pii/README.md, Stand Q-DE); no validator changes.pii_l1.jsonlgains 40 rows (20 positives, 20 hard negatives, DE and EN); two B-DE1 hard negatives that named a lowercase KVNR now carry a failed check digit. Detection change (balanced/recallonly;strictoutput is byte-identical): bare lowercase KVNRs and bare slashed Steuernummern are new findings. Dev scoreboard vs. cdacd9a (PASS in strict and balanced, no new hard-negative FP, precision 100.0 on both IDs): balancedpresidio_favoredKVNR 80.0 → 100.0, Steuernummer 0.0 → 66.7;ark_authoredSteuernummer 94.2 → 98.1; L1 F1 balanced +0.1 pp (DE +0.2). The new regex is linear and bounded (fixed group lengths,/literal for the anchor gate).latency.py compare --engine ark-native100 KiB p95 145.4 → 148.9 ms (+2.4 %) and 138.9 → 141.7 ms (+2.0 %), both PASS. Golden eval: 1233/1233 positives, 1143/1143 hard negatives.
Added¶
- Synthetic DLP gold for the credential, key and content entities without gold (GOLD-D6).
Seven entities without an ARK rule join
baseline_entitiesindlp_synth/rules.json:credential.cli_password(gitleakscurl-auth-user: the password ofcurl -u ci-deploy:… -T build.tgz https://…),credential.file.nuget_config(nuget-config-password:<add key="ClearTextPassword" value="…" />),credential.kubernetes_secret(kubernetes-secret-yaml: base64data:value of a one-linekind: Secretmanifest),credential.high_entropy_string(detect-secretsBase64HighEntropyString/HexHighEntropyString: base64 of 33 bytes, 48 hex digits, redrawn from the same seed until the Shannon entropy is above the plugin limit of 4.5 or 3.0 bits; generator fieldentropy_abovein Python and Rust),key.age_secret_key(age-secret-key:AGE-SECRET-KEY-1+ Bech32 of 32 bytes with a valid checksum),key.pkcs12_file(pkcs12-file: a one-line base64 RFC 7292 PFX in the OpenSSL layout, random salts, MAC and encrypted bags, not a usable key store) andkey.symmetric_key(no baseline rule, vendor docs: LaravelAPP_KEYbase64:…, Fernet key,openssl rand -hex 32). Each has one lookalike: a curl user without password, the NuGetUsernameentry, a ConfigMapbinaryDatavalue, a UUID v4, an age public key, a public X.509 certificate, a 40-hex digest. Per split and entity the carriers give 44 positives (DE and EN) and 220 hard negatives. Generators gain thebech32checksum segment (BIP 173, HRP from the preceding literal; Pythonrecipes.checksum, Rustgolden_recipes), and a format source may bedocs:<url>for an entity no baseline detects. Content classes get document gold in new carrier families, DE and EN in both splits: MySQL, PostgreSQL, MariaDB and SQLite dumps (formatsql, labelscontent.database_dumpandcontent.sql) and mails, tickets and chats with internal revenue, margin, forecast and salary figures (content.business_metric); log families already carrycontent.system_log.scoreboard_snapshot.pyscores a content class without span gold or external goldens on these carrier documents, socontent.system_log,content.database_dumpandcontent.business_metricleave "no gold".run_baselines.pycuts the name from gitleakskubernetes-secret-yamlfindings (stepyaml_entry) and namessqlrowsdump.sql. Detection change: none (evaluation data only, the Rust detector is untouched, no latency effect); the frozen corpora change only when the operator re-freezes them. Dev rows of the current generator, exact F1: ARK 0 on the seven value entities (no rule; its generic rules report up to all 44 values under another entity, the Kubernetes Secret value asdlp_password_assignment, which also reports its 88doc_example/test_keyvalues; a finding for G05); best baseline 50.0 age secret key, 44.6 curl password, 14.0 Kubernetes Secret (gitleaks, which matches the latter only in.yamlfiles and NuGet passwords only innuget.config), 25.8 high-entropy string (detect-secrets), 0 for PKCS#12 (a path-only gitleaks rule) and the symmetric key. ARK opt-in document F1:content.business_metric100,content.database_dump99.7,content.sql100,content.system_log0 (the log families hold no ERROR/FATAL line or stack trace). -
Austrian, Swiss, Portuguese and Irish national numbers (B-EU2: B06, B07, B14), one label each with a check digit or letter:
AT_SOCIAL_SECURITY_NUMBER(validators::at_social_security_number, VersicherungsnummerLLLC DDMMYY: serial from 100, the fourth digit the others weighted 3-7-9-5-8-4-2-1-6 mod 11, remainder 10 never issued; the date is not checked, since days above 31 and months above 12 are issued when a day runs out of serials),CH_AHV_NUMBER(ch_ahv_number, AHVN13: 756 and an EAN-13 check digit),PT_NIF(pt_nif, nine digits with the mod-11 check of the Portuguese VAT ID, now shared aspt_nif_check) andIE_PPSN(ie_ppsn, seven digits, the check letter of the Irish VAT number, now shared asie_check_letter, and an optional second letterA/Bcounted orW/T/Xnot).pii_social_security_number_at,pii_ahv_number_ch,pii_nif_ptandpii_ppsn_iereport the value in every mode when a term of the label stands near it or is its key, also as a CSV cell next to numeric cells (new lexicon entriespii.at_social_security_number,pii.ch_ahv_number,pii.pt_nif,pii.ie_ppsnwith DE, FR, IT and PT terms such asVSNR,AHV-Nummer,numéro AVS,número de contribuinte,PPSN; the AT and CH numbers also takeSozialversicherungsnummer). Inbalanced/recallthe printed forms are also reported bare at 0.55 (K05): the Austrian number1237 010180with a calendar date and the dotted AHV number756.1234.5678.97. Ten or thirteen bare digits, the nine digits of a NIF and seven digits with a letter (PPSN) need the term in every mode; nine digits afterPTare the body of a VAT ID and never a NIF. Four newark_onlybenchmark ids (pii.at.social_security_number,pii.ch.ahv_number,pii.pt.nif,pii.ie.ppsn); the PT NIF is its own id and M3 unit apart frompii.pt.vat_id(B16). M3 count (count_validated.py) ARK 59 → 63, shared unchanged at 22.pii_l1gains 99 rows (DE, EN, plus FR rows for the AHV number andptrows for the NIF;ptis a new golden language, allowed for the PT ids only); the frozen E05 goldset does not generate the four ids (NOT_GENERATED). Detection change: instrictthe four labels with a term or key are new findings; inbalanced/recallalso the two bare forms above. Dev scoreboard vs. cfb94cf: PASS in strict and balanced, no new hard-negative FP, every F1 ±0.0 (no dev gold for the new ids, no new findings on the dev corpora). Latency (latency.py compare --engine ark-native, three interleaved head/base pairs): 100 KiB p95 171.2 → 183.8, 185.1 → 168.3 and 168.3 → 167.1 ms (+7.4 %, −9.1 %, −0.7 %, all PASS), mean 94.1 → 92.2 ms (−2.0 %), from six more regex rules with fixed-length patterns behind the existing digit gates; the workloads report the same spans. Golden eval: 1630/1630 positives, 1480/1480 hard negatives.rust/tests/synchronous_core.rsnow counts the 1190 DLP rows and 529 recipes that DLP T15 checked in. -
Swedish, Canadian, Turkish and South African national numbers (B-LUHN: the checksum parts of T13 and T09), one label each with a check digit:
SE_ORGANISATIONSNUMMER(validators::se_organisationsnummer, third digit 2 or more, Luhn),CA_SIN(ca_sin, Luhn, first digit not 0 or 8),TR_NATIONAL_ID(tr_national_id, T.C. Kimlik No: no leading zero, tenth digit 7 × odd places − even places mod 10, eleventh the digit sum mod 10, ten repeated digits rejected) andZA_ID_NUMBER(za_id_number, birth date, citizenship 0–2, an 8 or 9 and Luhn).pii_organisationsnummer_se,pii_sin_ca,pii_national_id_trandpii_id_number_zareport the value in every mode when a term of the label stands near it or is its key, also as a CSV cell next to numeric cells (new lexicon entriespii.se_organisationsnummer,pii.ca_social_insurance_number,pii.tr_national_id,pii.za_id_numberwith Swedish, French, Turkish and Afrikaans terms such asorg.nr,numéro d'assurance sociale,T.C. Kimlik No,TCKN). Inbalanced/recallthe printed forms are also reported bare at 0.55 (K05): the organisationsnummer556703-7485, the SIN in hyphenated 3-3-3 groups (130-692-544) and the South African ID number (8001015009087,800101 5009 087). Ten bare digits, a SIN in 3-3-3 groups by spaces (the print form of a SIREN or a BSN) and the eleven digits of a T.C. Kimlik No need the term in every mode;TR_NATIONAL_IDcompetes with the Steuer-ID in the scored modes (K07). The four presidio-only idspii.se.organisationsnummer,pii.ca.sin,pii.tr.national_idandpii.za.id_numberbecomeshared; M3 count (count_validated.py) ARK 55 → 59, shared 19 → 22 (Presidio's CA SIN check is aninvalidate_resultand not counted). The format-only parts of T13 and T09 (TR licence plate, US MBI) stay out of the programme.pii_l1gains 106 rows (DE, EN, plus SV, TR and FR rows for the national field names;svandtrare new golden languages, allowed for the country ids only); the CREDITCARD hard negatives that are South African ID numbers now annotate them, and the rule fixture uses Korean resident registration numbers for that birth-date case instead. Detection change: instrictthe four labels with a term or key are new findings; inbalanced/recallalso the bare forms above. Dev scoreboard vs. a742f80 (PASS in strict and balanced, no new hard-negative FP, precision 100.0 on all four ids): L1 F1 strict +1.3 pp, balanced +1.2 pp (presidio_favored +0.9 / +1.5, neutral ±0.0, ark_authored +2.5 / +2.0); exact F1ark_authored100.0 for all four ids in both modes;presidio_favoredbalanced organisationsnummer 50.0, SIN 57.1, T.C. Kimlik No 66.7, ZA ID number 100.0 (the remaining FN are bare ten-, nine- and eleven-digit values and SINs in spaced groups without a term). Latency (latency.py compare --engine ark-native, three interleaved head/base pairs): 100 KiB p95 165.2 → 182.7, 177.9 → 172.3 and 183.2 → 169.2 ms (+10.6 %, −3.2 %, −7.6 %; two base runs alone differ by 7.7 %), mean 175.4 → 174.7 ms (−0.4 %), from seven more regex rules with fixed-length digit patterns. Golden eval: 1576/1576 positives, 1435/1435 hard negatives. -
Licence plates: plate field names, DE hyphen forms and UK plates (T30).
pii_kfz_kennzeichen_deand the newpii_vehicle_registration_ukshare one field part (ark-core/src/detectors/pii/licence_plate.rs): DE/EN plate field names before the value (Kfz-Kennzeichen,Kennzeichen,Nummernschild,Number plate,licence plate,vehicle registration,car reg,My car, registration …) joined by:,|, a tab,is,ist,lautet,asoron file is(also(Kennzeichen): …), or as the structural key of the value (K04:kennzeichen,kfzKennzeichen,license_plate,licensePlate, CSV headers). The DE value may carry a hyphen before the digits (K-MH-2476,H-J-6815E) and now has a form check (validators::de_licence_plate: at most eight characters without separators and suffix, no leading zero in the number, FZV § 8; base 25 + format 15 keeps its score at 0.65); the UK current form (AB51 ABC,BD62XYZ) needs an issued age identifier (validators::uk_vehicle_registration: 02–29 or 51–79, noI/Q, noZamong the area letters, as presidio-analyzer validates it). Neither form has a check digit and there is no district-code list (F16 stays deferred), so M3 is unchanged (de_licence_plateanduk_vehicle_registrationclassified as form only) and a value without a plate field name stays silent in every mode, also the bare TR, IN and NG values of the presidio-analyzer tests (T27 still open). A genericregistration numbernames no plate. Newpii_l1rows (19 positives, 15 hard negatives, DE + EN; the known gapLicense plate: F-EZ 67Eis now gated), rule fixture with structure probes, tests inrust/tests/pii_licence_plates.rs. Detection change (every mode): plate values after these field names and keys, DE plates with a hyphen before the digits and UK plates are newLICENSEPLATEfindings; plate values must be uppercase as printed and pass the form check (Kennzeichen: ab-cd 12,ABC-DE 1234,B-AB 0123are no longer reported instrict). Dev scoreboard vs. c8d6cfe (PASS in strict and balanced, no new hard-negative FP): licence plate exact F1ark_authored34.8 → 100.0 (19 of 19) andpresidio_favored0.0 → 9.1 (My car reg is AB51 ABC) in both modes, all families 18.2 → 66.7; L1 F1 +0.2 pp in strict and balanced. The other 20presidio_favoredFN are bare TR, IN, NG and old UK prefix/suffix values without a plate term (T27). The regexes are linear and bounded (fixed repetition counts, ASCII boundaries).latency.py compare --engine ark-native, two runs: 100 KiB p95 158.3 → 156.8 ms (−1.0 %) and 162.4 → 158.4 ms (−2.4 %), PASS; 4 KiB p95 +8.7 % / +8.9 % (not gated), while an in-process A/B on the 4 KiB DE workload with both plate rules disabled shows no measurable difference (median 4.67 ms either way). Golden eval:pii_kfz_kennzeichen_de14/14 DE, 12/12 EN,pii_vehicle_registration_uk2/2 DE, 5/5 EN. -
GB BBAN as account number (T29). New context-bound rule
pii_bban_gbreports four letters of the bank code, the sort code and the account (ZFJR88590163492773,validators::uk_bban: form only, no account of zeros or one repeated digit) asFINANCIAL_ACCOUNT_NUMBERin every mode when a BBAN or account term of the K03 lexicon stands near the value or is its structural key (K04):bank account number (BBAN) …,our bank account number, …,… (BBAN),Bankkonto des Mieters: …, JSON/CSV keys such asbankkontoorbankingAccount, which the field-bound account rule does not bind. The form has no check digit, so there is no context-free form: a bare value and 18-character codes after order, reference, tracking or VIN field names stay silent, as does a value with a nearer counter-context term. M3 unchanged (uk_bbanis classified as form only). Newpii_l1rows (12 positives, 14 hard negatives, DE + EN), one rule fixture, tests inrust/tests/pii_account_numbers.rs. Detection change (every mode): BBAN-shaped values near a BBAN or account term are newFINANCIAL_ACCOUNT_NUMBERfindings. Dev scoreboard vs. 94aca75 (PASS in strict and balanced, no new hard-negative FP): Gretel (neutral) account number exact F1 balanced 46.7 → 49.2 (4 new TP, 0 new FP), all families +2.0 pp in strict and balanced; customer ID, employee ID and username unchanged; L1 F1 +0.1 pp in both modes. The remaining BBAN-shaped FN have no account term (addresses, SWIFT MT tags, EDIFACT, XBRL), a misspelled field (Mother' BBN:) or sit in a truncated CSV row. The regex is linear and bounded (fixed lengths, ASCII boundaries); the context check only runs on the rare shape matches.latency.py compare --engine ark-native: 100 KiB p95 165.1 → 165.9 ms (+0.5 %, PASS). Golden eval:pii_bban_gb6/6 DE, 6/6 EN. -
Account numbers (B-ACCT: A04, F14). The US ABA routing number gets its check digit (
validators::aba_routing_number: Federal Reserve routing symbol 00–12, 21–32, 61–72 or 80, weights 3-7-1 mod 10, not one repeated digit) and reportsFINANCIAL_ACCOUNT_NUMBER.pii_aba_routing_number_usreports nine digits or the 4-4-1 form in every mode with a routing or account term near the value or as its key (new lexicon entrypii.bank_routing_number:ABA,RTN,routing number,Routingnummer,numéro de routage…), never as part of a longer number;pii_aba_routing_number_us_context_freereports the bare 4-4-1 form (0711-0130-7) inbalanced/recallat 0.55. Nine bare digits pass the check one time in ten and stay silent. The field-boundpii_financial_account_numberalso bindsBBAN,Bank Routing Number,Routing-Nummer,ABA number,Acct No/Acct IDand FR/ES/IT routing field names, before the value and as structural keys (JSONbban, CSVbank_routing_number, XML<AcctNumber>,<RoutingNumber>,<AcctId>,numeroCuentaBancaria), with the form check as forKontonummer;pii_customer_idtakescustomer_id/customer-numberin running text. M3 count (count_validated.py) ARK 49 → 50 (pii.financial_account_number,us, shared with Presidio'sABA_ROUTING_NUMBER); the scoreboard ports the validator. Newrust/tests/pii_account_numbers.rs(vectors incl. the presidio-analyzer values), 77 newpii_l1rows (DE + EN), two rule fixtures. Detection change: in every mode a routing number with its term or key, and BBAN, routing andacctfields (also as keys), are newFINANCIAL_ACCOUNT_NUMBERfindings, andcustomer_id:values newCUSTOMER_IDfindings; inbalanced/recallthe bare 4-4-1 routing number is new. Dev scoreboard vs. 84c8f5a (PASS in strict and balanced, no new hard-negative FP): account number exact F1 balanced neutral 8.2 → 46.7 (Presidio tuned 12.5), presidio_favored 0.0 → 57.1, ark_authored 72.2 → 75.7; customer ID neutral 46.7 → 51.1; employee ID and username unchanged; L1 F1 strict +0.7, balanced +0.7 pp (neutral +1.6). Latency (latency.py compare --engine ark-native): 100 KiB p95 160.9 → 150.5 ms (−6.5 %, PASS), 4 KiB −4.7 %, short 0.234 → 0.261 ms (+11.6 %, not gated). Golden eval: 1256/1256 positives, 1157/1157 hard negatives. - French, Dutch and Belgian national numbers (B-EU1: B09, B10, B11), one label each with a check
digit:
FR_NIR(validators::fr_nir, numéro de sécurité sociale: key 97 − (first thirteen mod 97) with Corsica2A/2Bread as 19/18, plus sex, birth month and département),FR_SIREN(fr_siren, Luhn),FR_SIRET(fr_siret, a valid SIREN and Luhn over fourteen digits, La Poste356 000 000by digit sum mod 5),NL_BSN(nl_bsn, burgerservicenummer, 11-proof, shared with the former Dutch btw-id check) andBE_NATIONAL_NUMBER(be_national_number, rijksregisternummer / numéro de registre national: birth date incl. BIS months, serial 001–998, 97 − (first nine mod 97), births since 2000 with a2in front).pii_nir_fr,pii_siren_fr,pii_siret_fr,pii_bsn_nlandpii_national_number_bereport the value in every mode when a term of the label stands near it or is its key (new lexicon entriespii.fr_social_security_number,pii.fr_siren,pii.fr_siret,pii.nl_citizen_service_number,pii.be_national_numberwith FR and NL terms), never as part of a longer number. Inbalanced/recallthe forms whose shape adds to the check digit are also reported bare at 0.55 (K05): a NIR printed 1-2-2-2-3-3-2 or as fifteen characters of sex 1 or 2 (pii_nir_fr_context_free), a SIRET in 3-3-3-5 groups (pii_siret_fr_context_free), a BSN printed1234.56.782(pii_bsn_nl_context_free) and a national number printed85.07.30-033.28(pii_national_number_be_context_free). The SIREN and bare runs of nine, eleven or fourteen digits need the term in every mode.FR_NIR,FR_SIRETandBE_NATIONAL_NUMBERcompete with card numbers and the Steuer-ID in the scored modes (K07); a SIRET in its 3-3-3-5 print groups is never a card number (context::card_position: no card scheme prints that grouping, while a quarter of all SIRETs pass Luhn with a Maestro or Diners prefix). Five newark_onlybenchmark IDs; M3 count (count_validated.py) ARK 50 → 55. Detection change: new findings of the five labels (strict: values with a term or key; balanced/recall: also the bare printed forms);pii_credit_cardno longer reports fourteen digits in 3-3-3-5 groups. Dev scoreboard vs. 81ecc0e: PASS in strict and balanced, no new hard-negative FP, every F1 ±0.0 (the dev corpora carry no gold for the five IDs and none of the rules fires there). Golden eval: 1282/1282 positives, 1183/1183 hard negatives. Latency (latency.py compare --engine ark-native): 100 KiB p95 159.3 → 159.0 ms (−0.2 %, PASS); 4 KiB p50 1.88 → 1.80 ms.
[0.1.13] - 2026-10-26¶
Fixed¶
- Restore accelerator feature builds with ORT rc.13 by using its current execution-provider API, while preserving the rc.10 compatibility path used for macOS Intel wheels.
- Allow combined accelerator-feature builds to link against an available ORT distribution; explicitly requested unavailable backends still fail during scanner initialization.
- Fix tokenizer-cache synchronization permissions and preparation-lock contention handling on Windows; active preparations remain subject to the configured deadline.
- Replace Core's
dirsdependency with MIT OR Apache-2.0homefor the same Hugging Face token-cache lookup. Core download support no longer pulls in MPL-2.0option-ext.
Changed¶
- No IPv6 sub-span after a word that ends in a hex group (T28, TRIAGE 2026-W44 bucket 5). The
IPv6 rules took any word before
:as a field name, sofoo2001:db8::10.0.0.1reporteddb8::10.0.0.1, against the B-IP convention "no partial match". A field name before an IPv6 address (ipv6_opener!inark-core/src/detectors/pii/common.rs, shared by the three IPv6 rules) no longer ends in a hex run of three or more characters with a digit; such a run is a further:-group glued to the word (foo2001,host2001,sku4f00).IPv6:,inet6:,eth0:,ens33:andIP-Adresse:still open the value; the IPv4 tail stays rejected as before.pii_l1.jsonlgains 20 rows (T28_IP_CASES, five positives and five hard negatives per language), the rule fixture a DE and an EN hard negative. Detection change: (PII L1, all modes) an IPv6 candidate behind such a word is no longer reported. Dev scoreboard vs. base (PASS in strict and balanced, no new hard-negative FP): presidio_favored IP exact precision 96.9 → 100.0 and exact F1 84.9 → 86.1 in both modes; the gold10.0.0.1offoo2001:db8::10.0.0.1stays a known FN, so overlap F1 there drops by the one overlapping hit (IP over all families exact +0.2, overlap −0.2 pp); every other entity unchanged. The opener stays linear and adds no rule.latency.py compare --engine ark-native, three head/base runs: 100 KiB p95 147.7 → 157.8 (+6.8 %, during a parallel build), 139.4 → 151.1 (+8.4 %) and 147.0 → 145.6 ms (−1.0 %), all PASS; the spread is noise. - E-mail addresses with Unicode local parts, apostrophes and IDN domains; no address in URL
userinfo; IANA top-level domains (B-EMAIL: T06, T23, F05, baseline bucket P7).
pii_emailtook ASCII local parts only, somoeleprêtr@outlook.combecametr@outlook.com,jürgen.müller@beispiel.debecameller@beispiel.de,o'neil@example.combecameneil@example.comand诗向@yahoo.comwas no match; it cuta@xn--d1acufc.xn--p1aiafter.xn, took any letter run as top-level domain (alicewilliams@email.note) and reported the password ofpostgres://user:pass@hostas an address. A local part now holds Unicode letters, digits and marks (RFC 6531) and apostrophes between them (never'@); Han, hiragana and katakana separate no words, so a local part of their own is at most three ideographs or katakana (联系邮箱伟伟许@aol.com→伟伟许@aol.com). Domain labels hold Unicode letters (info@müller-bau.de), the top-level domain is ASCII letters or punycode (xn--p1ai), and the new validatorvalidators::email_domainaccepts it only if IANA delegates it (ark-core/src/detectors/pii/iana_tlds.txt, version 2026102500) or it is a reserved placeholder of the gold convention T01 (.test,.example,.invalid,.localhost).context::url_userinforejects a candidate inside the userinfo of a URL (scheme://user:pass@host,ssh://git@host); credentials there stay DLP findings, an address in a URL path or query stays. The rule has format strength now; its base score drops from 70 to 55, so the score without context stays 0.70.pii_l1.jsonlgains 22 rows (12 positives, 10 hard negatives, DE and EN), the rule fixture six hard negatives and a span per language; the newrust/tests/pii_email.rscovers spans, code-point offsets, userinfo, quoted at signs, the TLD list and placeholders. Detection change: (PII L1, all modes) addresses with non-ASCII or apostrophe local parts and punycode TLDs are new findings or longer spans; addresses with a TLD that IANA does not delegate (.note,.intern,.internal,.local) and candidates in URL userinfo are no longer reported. Dev scoreboard vs. base (PASS in strict and balanced, no new hard-negative FP, no other entity changes): email exact F1 in balanced neutral 67.4 → 68.8 (Presidio tuned 67.9; OpenPII Nano 94.5 → 97.7, Gretel 52.2 → 52.4), presidio_favored 93.8 → 100.0 (tuned 100.0), ark_authored unchanged at 100.0; L1 F1 +0.2 pp in both modes (neutral +0.4, presidio_favored +0.3). The regex stays linear and its literal@still prefilters it: on 100 KiB without@a scan takes 1.3 µs before and after, with about 1,700 addresses per 100 KiB 133 → 195 µs.latency.py compare --engine ark-native100 KiB p95 147.1 → 146.9 / 157.9 ms in two head runs (PASS, noise), 4 KiB 7.66 → 6.80 / 6.62 ms.
Detection changes¶
- DLP L1 DevOps and package-registry tokens (P05, all modes): eleven new provider rules, all
SECRET_TOKENwith subtypeaccess_tokenand a format source inresearch/dlp/rule_sources.yaml.dlp_dockerhub_token(dckr_pat_/dckr_oat_, 27 base64url characters),dlp_pypi_token(pypi-and a base64url Macaroon V2 forpypi.orgortest.pypi.org, parsed completely by the newvalidators::macaroon_v2_parse,structurestrength, TestPyPI with severitytest),dlp_rubygems_token,dlp_cratesio_token(cioand 32 alphanumerics with both cases),dlp_packagist_token(Private Packagist prefixes, 60 hex digits and their CRC32, newvalidators::crc32_hex_valid,checksumstrength; a generic rule does not report a Packagist shape whose CRC32 fails),dlp_jfrog_token(AKCp,cmVmd),dlp_terraform_cloud_token(.atlasv1.),dlp_pulumi_token,dlp_vault_token(hvs.,hvb.),dlp_vault_legacy_token(s.and 24 characters, only withvaulton the value's line or up to five lines above) anddlp_doppler_token(all documented Doppler prefixes). The seven GOLD-D3 entities for PyPI, RubyGems, JFrog, Terraform Cloud, Pulumi, Vault and Doppler becomeshared; Docker Hub, crates.io and Packagist are newark_onlyentities. NuGet API keys have no vendor format source and get no rule. Detection change: these tokens are new findings; a value a generic rule reported before now carries the provider rule. Dev scoreboard vs. d4148d7 (PASS, 67 checks): every group unchanged (the frozen dev corpora predate the GOLD-D3 rows, T19), FP/MB on benign code 0.053 → 0.053, hard-negative FP 154 → 154; 100 KiB p95 11.06 → 11.03 ms, 10 MiB repo 7.68 → 7.69 MiB/s.dlp_l1.jsonlgains 48 rows (25 positives, 23 hard negatives, DE + EN), new generators with lookalikes and the recipe checksum kindcrc32_hex; newrust/tests/dlp_registry_tokens.rs. - DLP L1 (T17, all modes):
dlp_url_userinfo_passwordreads the matched userinfo password percent-decoded before the F-GEN1 likeness check and also drops credential nouns masked with leetspeak digits and symbols, as Basic auth and connection strings already do. Test credentials such ashttp://user:p%40ss@proxy.comandscheme://me%40company:p%40ssw%24rd@…(composer docs and tests) are no longer reported; chosen passwords with the reserved characters%40%24%3Astay findings with the encoded span. Detection change: dev benign-code FP 5 → 2 (FP/MB 0.1324 → 0.053), exact F1 87.2 and hard-negative FP 154 unchanged; the decode runs only on a matched value containing%, 100 KiB p95 12.45 → 12.19 ms (noise). New synthetic generatorreserved_char_password(inserted URL-encoded through the newurl_encodedkey indlp_synth/rules.json, not used by carriers) and 20 golden rows. -
DLP L1 (T17 review, all modes):
dlp_url_userinfo_passwordchecks for thex-oauth-basicpseudo-password on the encoded password, so an encoded%3Astays password data (postgres://app:Kx7pQ2vL%3Ax-oauth-basic@…is reported again, as before T17). Detection change: restores findings the decode had dropped; the pseudo-password check now runs before the decode. -
Shell risk: agent-shell robustness and heredoc handling. Pin
cat-shell-risk-commanderrevision30d77a80f1ddb3c5c63d9b24c6f5083b15057813(format_version: 2, about 4.4 MB). The updated model reduces false positives for routine agent scripting andgh/gitpublish commands. Detection change: recalibrated model policies useexternal_commands_1pct_fpr0.474714 andblock_0_1pct_fpr2.517865. Code/data heredoc bodies feeding non-shell interpreters or files are masked only in the shell-token feature block; character features retain the original text. Heredocs belong to their owning command, unquoted bodies with shell expansions remain visible, and complete punctuated delimiters preserve following commands. Rust and Python regression tests cover substitutions, backticks and quoted bodies; scikit-learn score parity includes seven new cases and unchanged existing reference scores.
Added¶
- Nordic personal identity numbers (B-NORD: B12, B13), one label each with a check digit or
character:
SE_PERSONNUMMER(validators::se_personnummer, Luhn overYYMMDD-NNNC,+from the 100th birthday, ten or twelve digits, coordination numbers with day + 60),DK_CPR(dk_cpr, weights 4-3-2-7-6-5-4-3-2-1 mod 11, century from the seventh digit),NO_FODSELSNUMMER(no_fodselsnummer, two check digits mod 11, D- and H-numbers) andFI_PERSONAL_IDENTITY_CODE(fi_hetu, check character mod 31 with the century signs-,Y–U,A–F, either case). Births before 1900 are rejected.pii_personnummer_se,pii_cpr_dk,pii_fodselsnummer_noandpii_hetu_fireport the value in every mode when a term of the label (Personnummer,CPR,Fødselsnummer,fnr,HETU,henkilötunnus; new lexicon entriespii.se_personnummer,pii.dk_cpr,pii.no_fodselsnummer,pii.nordic_personal_number,pii.fi_personal_identity_code) stands near it or is its key, also as a CSV cell next to numeric cells. Inbalanced/recallthe printed forms are reported bare at 0.55 (K05):YYMMDD-NNNC(811228-9874),DDMMYY-SSSS(080572-3300),DDMMYY NNNKK(110897 33660) and the henkilötunnus (131052-308T). Bare digit runs and twelve digits with a dash (date stamps such as20241026-1529) need the term. The CPR validator requires the mod-11 sum, so CPR numbers issued without it since 2007 stay unreported and the unit counts as checksum-validated. The four labels compete with each other and withSTEUERIDin the scored modes (K07).pii.se.personnummerandpii.fi.personal_identity_codebecomeshared;pii.dk.cprandpii.no.fodselsnummerare newark_onlyids; M3 count (count_validated.py) ARK 45 → 49. The PHONE hard negative811228-9874(a valid personnummer) becomes811228-9875. Detection change: instrictthe four values with a term or key are new findings; inbalanced/recallalso the bare forms above. Dev scoreboard vs. a202253 (PASS in strict and balanced, no new hard-negative FP, precision 100.0 on both measured ids): L1 F1 strict +0.7, balanced +0.9 pp (presidio_favored +3.0, neutral ±0.0); balanced F1 personnummer 100.0, henkilötunnus 98.8, strict 89.5 and 80.0 (all 0.0 before). Latency (latency.py compare --engine ark-native): 100 KiB p95 147.0 → 154.0 ms (+4.8 %, PASS), from eight more regex rules over the text; 4 KiB p95 6.9 → 5.6 ms (noise). Golden eval: all gated positives and hard negatives pass;pii_l1.jsonlgains 88 rows. - DLP L1 overlap attribution in the generic tier (T15): where
dlp_env_var_secret(subtypegeneric_secret) anddlp_password_assignment(subtypepassword) report the identical value span of a password variable (DB_PASSWORD=…,ENV POSTGRES_PASSWORD=…,export SMTP_PASSWORD=…, CIenv:/variables:, composeenvironment:, code constants such asDB_PASSWORD = "…"), the rule with the more specific subtype keeps the span (dlp::overlap). Detection change: spans, labels and scores are unchanged; such values are now attributed todlp_password_assignmentanddlp_env_var_secretstays inmatched_ruleswithsuppressed_by: dlp_password_assignment. Variables without a password noun (API_KEY,CLIENT_SECRET,TOKEN) staydlp_env_var_secret, and where the two rules report different spans the earlier rule keeps its span as before.dlp_l1.jsonlgains 22 rows (12 positives, 10 hard negatives, DE and EN) in env, Dockerfile, CLI, CI YAML and code carriers.
[0.1.12] - 2026-10-25¶
Detection changes¶
- PII L1: new DE, UK, AU and SG identifiers; stronger German check-digit validation;
broader SSN context handling; NANP phone extensions included in findings. Eleven further
VAT-country labels are available in
balanced/recall. Mode-specific gates and exceptions are documented below. - DLP L1: credential values in nested YAML/JSON/TOML and HCL/XML are detected with structure-aware spans, plus connection-string, LDAP-bind and Redis passwords. Connection passwords have more precise spans and overlap ownership; mixed non-secret lists and digest-like values are excluded.
- Dedicated Wolf: document decisions now use normalized logit Smooth Max at temperature 0.1. Existing 254-content-token windows and model assets remain supported. Applications should review their configured probability thresholds for this intentional behavior change.
- Shell risk: the updated model distinguishes download-and-execute commands from plain downloads into files or filters. Its model revision and calibrated thresholds change; the detailed entry below documents the detection and block-level recall changes.
Synchronous Core¶
- Extract shared native PII/DLP/PI implementations into the MIT OR Apache-2.0
patronus-ark-corecrate. - Add queue-free scans, explicit resource limits and local inference with Wolf Defender.
- Correct dedicated Wolf document pooling to normalized logit Smooth Max (temperature 0.1); thresholds remain application policy. The 254-content-token profile is checked against an independent tokenizer/direct-ORT oracle; archived 256-content-token PyTorch document references do not establish parity for that profile.
- Reuse existing Wolf assets and revisions, with selectable FP16 or INT8/INT4 exports. Preserve Ark's 254-content-token windows plus two boundary tokens; share the process-wide compact tokenizer cache and reuse each scanner's own warmed Wolf session.
- Add typed cancellation and deadlines to Core scans, including cooperative termination of active ONNX Runtime runs. Existing scanner entry points remain available.
- Add explicit asset preparation through
warmup::prepare_assetsandScanner::warmup, using the Wolf cache preparation shared with full Ark.DownloadPolicy::Neveris the default;Missingrequires thedownloadfeature to fetch absent files from the existing pinned revision. Preparation creates no tokenizer or ONNX session; warmup constructs the reusable scanner and verifies the configured backend with an initial inference. Subsequent scans remain offline. The preparation timeout covers cache-lock waiting, downloads and retries, not model construction.
Changed¶
-
Shell risk: download-into-interpreter relations (#159).
shell_commandnow pinscat-shell-risk-commanderrevisionf6243388(stillformat_version: 2), whose shell-token block adds five relation markers from a small, depth-limited parse of the command:DL->PIPE->INTERP(curl … | bash,irm … | iex),DL->SUBST->INTERP(bash <(curl …),source <(…),sh -c "$(curl …)",iex (iwr …),.DownloadString(…)),DL->FILE->EXEC(a downloaded file that the same command runs),DL->PIPE->FILTER(curl … | jq) andDL->FILE(download to a file that is never run). Nested-c/-Commandscripts, backticks, newlines and#comments are followed; nesting deeper than 32 levels yields no markers.shell_tokens.rsports the reference bit for bit (full-corpus feature parity on 36,391 texts; decision scores within 1e-6). Detection change: on the issue #159 set all 27 download-and-execute commands flag (was 23;bash <(curl …),source <(curl …)and the IP variant ofcurl … | bashwere missed) and none of the 22 plain downloads to a file or filter does (was 8). New thresholds:external_commands_1pct_fpr0.614926,block_0_1pct_fpr2.538794 (true 0.1 % FPR on held-out data; the old block threshold ran at ~0.35 %, so block-level recall drops at that stricter operating point while recall at matched FPR is unchanged or higher). Pure downloads without execution (wget https://…/x.tar.gz) no longer flag. Older runtimes keep revision2ba751f9, which they pin. -
NANP extensions inside the phone span (F18, baseline bucket P5, candidate C3).
pii_phone_usended with\d{4}\b, which never holds before an attachedx, so345-899-3560x4587was no match, andpii_phone_international, the earlier rule, cut+1-604-696-5272x565to+1-604-696. An extension right after a NANP number (x4587,ext 123,ext. 123,extension 12, any letter case, one blank before it) now belongs to the span ofpii_phone_us;pii_phone_internationaltakes it only after+1and ten digits, so other country codes end where they ended before (+49 30 1234567 x12→+49 30 1234567). A barexorextwithout digits ends the span before it (212-555-0147 ext (ask reception)), and letters attached to the number (212-555-0147xl) still yield no match.validators::unanchored_phonechecks the number without its extension, so repeated digits (999-999-9999 ext. 12) stay rejected and extension digits never push a number past 15 digits.pii_l1.jsonlgains 22 rows (12 positives, 10 hard negatives, DE and EN), the rule fixture two hard negatives; the newrust/tests/pii_phone_extension.rscovers spans, barex/ext, silent lookalikes and other country codes. Detection change: (PII L1, all modes) NANP numbers with an extension are new findings or longer spans that include the extension;+1numbers with an attached extension no longer yield the fragment before the last digit group. Dev scoreboard vs. base (PASS in strict and balanced, no new hard-negative FP, no other entity changes): phone exact F1 presidio_favored 68.5 → 80.5 in strict and balanced, neutral 81.8 → 82.0 (EN 84.9 → 85.3), ark_authored unchanged; L1 F1 balanced +0.1 pp (presidio_favored +1.4). Both regexes only gain an optional tail after the last digit group and stay linear; the prefilter and anchor gate are unchanged.latency.py compare --engine ark-native100 KiB p95 135.9 → 147.5 / 134.6 ms in two head runs (PASS, noise), 4 KiB 6.15 → 5.94 / 5.75 ms. - Read HCL and XML values under credential keys (K05, part 3: K05C).
dlp_env_var_secret,dlp_password_assignmentanddlp_generic_credential_assignmentbind an attribute in an HCL block (resource "…" "…" {,provider,locals, also nested blocks, objects andjsonencode({ … })) by its double-quoted string or array items; a bare value there is an expression (var.db_password,random_password.db.result) and binds nothing, while HOCON and code blocks without an HCL header stayproperties. A Terraform/Packervariable "db_password" {block binds itsdefaultthrough the variable name. In XML an element named by the key binds its text on the line (<password>…</password>, namespaced, also in CDATA), and a config element (add,entry,property,param,setting,string…) whosekey/nameattribute holds the key binds itsvalueattribute in any order or its text (<entry key="client_secret">…</entry>,<add value="…" key="ApiToken"/>). Element text with blanks or markup, empty elements, HTML form elements and XML inside a string or comment of code (Raw: `<password>…`,"<token>"+t+"</token>",/// <param …>) bind nothing; element text must suit a field value like a quoted one. The K05 value-shape filter now checks every config value under its bound key, also when the key stands apart from the value (variable "passwort_sha256",<hashed_password>), and an XMLvalueattribute under itskey/nameattribute (<add key="PasswordSha256" value="<hex>">). Matches carrystructurehclorxml; the XML tag grammar is shared with the PII K04 document structure.dlp_l1.jsonlgains 28 rows (12 positives, 16 hard negatives with the K05 shapes per format, DE and EN keys) and the carrierhcl; the newrust/tests/dlp_config_keys_hcl_xml.rschecks both formats. Detection change: XML element text, keyed XML elements, HCL variable defaults and//-commented HCL arrays under credential keys are new findings; an XMLvalueattribute holding a hex digest under a digest-named key is no longer reported by the generic rules. Dev benign code, benign docs, synthetic carriers and the external dev corpora are byte-identical, and so is a 322 MB probe of local code, XML, HTML and docs (Rust crates, Python packages,/usr/share). 100 KiB p95 11.19 → 10.78 ms (noise). Golden eval: 515/515 positives, 576/576 hard negatives. Validators are unchanged. - Detection change: (DLP L1, all modes) a keyed XML element binds its
valueattribute also with other attributes between it and thekey/nameattribute (<add key="ApiToken" type="string" value="…"/>); the value gate ofdlp_password_assignmentanddlp_generic_credential_assignmentno longer drops it. XML inside a comment over several lines (<!--on a line above, up to-->) binds nothing. HTML form elements stay silent. - Read YAML, JSON and TOML values and arrays under credential keys (K05, part 2: K05B).
dlp_env_var_secret,dlp_password_assignmentanddlp_generic_credential_assignmentbind a config value at any nesting depth: indented YAML mappings and list entries (- key: value), nested JSON objects and TOML tables. A JSON or TOML string runs to its closing quote past escapes (\"), and a plain YAML value with;,,or}inside runs to its inline comment instead of stopping at the first such character. An array under a credential key binds each string item: JSON and TOML arrays (also over several lines), YAML flow sequences ([a, "b"]) and block sequences (- itemlines); a plural key (api_keys,tokens,SERVICE_TOKENS) binds only items. An item must look like a secret on its own (likeness::is_item_value: printable ASCII without blanks, a digit or both cases, no template characters, no identifier, word, path or stand-in), and a list binds only if every item does, so tokenizer vocabularies, scopes and word lists stay silent; an array with numbers, objects or mappings binds nothing. No YAML value is a plain scalar with a YAML indicator at its start (alias, anchor, tag, block scalar, flow mapping), a word list (read,write), or a header field (X-Client-Secret:) outside an HTTP head. The K05 value-shape filter also rejects unquoted YAML aliases (*default_password) and values encrypted for the repository (SOPSENC[AES256_GCM,data:…],$ANSIBLE_VAULT;…), and checks the digest name of an item's own key (sha256_tokens). Matches carrystructureyaml,jsonortoml.dlp_l1.jsonlgains 37 rows (12 positives, 25 hard negatives with the K05 shapes per format, DE and EN keys); the newrust/tests/dlp_config_keys_nested.rschecks every format and shape. Detection change: array items and whole punctuated YAML values under credential keys are new findings, and JSON values with an escaped quote are reported whole (Tz8\"Wm4K…instead ofTz8\). Rust raw pointer types after apwd/passwdfield (pwd: *mut passwd) are no longer reported as passwords: on a 432 MB probe of local code and config (Rust crates, Python packages, docs) 102 such spans disappear and none is added. Dev benign code, benign docs and synthetic carriers are byte-identical; scoreboard gate on the dev corpora PASS with unchanged numbers: exact F1 87.2, FP/MB benign code 0.1324 (5 FP), benign docs 0.1327, hard-negative FP 154, 100 KiB p95 13.18 → 13.56 ms (+2.9 %). Golden eval: 503/503 positives, 560/560 hard negatives. Validators are unchanged. - A credential list binds nothing when one of its items has a non-secret shape (K05B, PR #152
review). Detection change (
dlp_env_var_secret,dlp_password_assignment,dlp_generic_credential_assignment): the all-or-nothing list check also applies the K05 value-shape filter (UUID, digest, object id) to every item, so intokens: ["550e8400-…", "Tz8Wm4K93RVqe7xAb2Lp"]the token-like item is no longer reported on its own.
Added¶
- Synthetic DLP gold for the 23 hosting, cloud and data entities without an ARK rule and without
gold (GOLD-D4):
cloud.aws.bedrock_api_key,cloud.alibaba.access_key,cloud.azure.entra_client_secret,cloud.azure.storage_account_key,cloud.gcp.service_account_key,cloud.ibm.credentials,cloud.yandex.credentials,cloud.scalingo.api_token,hosting.flyio.access_token,hosting.netlify.access_token,hosting.heroku.api_key,hosting.cloudflare.api_key,hosting.digitalocean.token,hosting.openshift.user_token,hosting.defined_networking.api_token,hosting.fastly.api_token,hosting.settlemint.access_token,data.databricks.api_token,data.planetscale.secret,data.confluent.secret,data.algolia.api_key,data.clickhouse.api_secret,data.cloudant.credentials(packages P03, P04, P06, P12).baseline_entitiesindlp_synth/rules.jsongains 37 generators in the format of each entity's gitleaks rule, or, for the four entities without one, of its TruffleHog detector (GCP) or detect-secrets plugin (AzureStorageKeyDetector,IbmCloudIamDetector,CloudantDetector): the Bedrock key isABSK+ base64 ofBedrockAPIKey-…-at-<account>:…, the Azure storage key is base64 of 64 bytes inside a connection string, the GCP service account key is a one-line key file with a PKCS#8 RSA 2048 private key (random integers, not a usable key), OpenShift tokens aresha256~+ base64url of 32 bytes. Each entity has one lookalike (wrong length, or a GCP workload identity federation config without a private key). Per split and entity the carriers give 40 positives (20 DE, 20 EN) and 200 hard negatives. The baseline test accepts TruffleHog and detect-secrets format sources for entities without a gitleaks rule, and the profile test covers the hosting, cloud and data families.entity_map.yamlmaps the TruffleHog detectors that report these formats (Azure,AzureStorage,Netlify,Heroku,CloudflareApiToken,CloudflareCaKey,DigitalOceanV2,FastlyPersonalToken;check-entity-mappasses on the pinned binaries).run_baselines.pycutsAccountKey=from detect-secretsAzureStorageKeyDetectorfindings (stepkey_name, sharing the name-prefix table withcookie_name) and widens TruffleHogGCPfindings, which report theclient_email, to the key file the detector matched (stepkey_file). Detection change: none (evaluation data only, no latency effect); the frozen corpora change only when the operator re-freezes them. Dev rows, exact F1: ARK 0 on all 23 (no rule; generic rules find up to 34 of 40 values); best baseline 100 Entra client secret (TruffleHog) and OpenShift (gitleaks), 98.7 Azure storage key, 96.1 Fastly, 94.7 Netlify (TruffleHog), 97.4 Fly.io, 66.7 ClickHouse, 65.5 Databricks (gitleaks), 63.9 Heroku, 60.7 Cloudflare (TruffleHog), 60.3 DigitalOcean (gitleaks), 49.7 GCP service account key (TruffleHog), 49.4 Bedrock, Scalingo, PlanetScale and SettleMint, 47.5 Alibaba, 45.7 Yandex, Algolia, Confluent and Defined Networking (gitleaks), 45.7 IBM Cloud and 45.0 Cloudant (detect-secrets). Baseline precision suffers mostly fromdoc_example/test_keyvalues. After a re-freeze ARK reports 119 hard-negative spans on these rows (dlp_generic_credential_assignmentanddlp_env_var_secreton example and test values; nolookalikehit) — findings for P03/P04/P06/P12. - Synthetic DLP gold for the 20 devops, ci, vcs and registry entities without an ARK rule and
without gold (GOLD-D3):
vcs.gitlab.session_cookie,vcs.bitbucket.client_secret,ci.droneci.access_token,ci.travisci.access_token,registry.jfrog.artifactory,registry.pypi.upload_token,registry.rubygems.api_token,registry.clojars.api_token,devops.terraform_cloud.token,devops.pulumi.api_token,devops.vault.token,devops.doppler.api_token,devops.prefect.api_token,devops.infracost.api_token,devops.octopus_deploy.api_key,devops.harness.api_key,devops.snyk.api_token,devops.sonar.api_token,devops.codecov.access_token,devops.sourcegraph.access_token(packages P05, P06, F13).baseline_entitiesindlp_synth/rules.jsongains 26 generators in the format of each entity's gitleaks rule (the PyPI token ispypi-+ base64url of a macaroon v2 with locationpypi.org, UUID identifier, caveat and 32-byte signature; Vault hashvs., legacys.and batchhvb.tokens; the GitLab session cookie sits in a_gitlab_session=wrapper) and one lookalike per entity (wrong length or character set, e.g. an OctopusAPI-key in lower case, a RubyGems key in upper-case hex, a truncated PyPI token). Per split and entity the carriers give 40 positives (20 DE, 20 EN) and 200 hard negatives.vcs.gitlab.tokenandvcs.gitlab.oauth_app_secretalready have carrier gold through their ARK rules; the snapshot of 2026-10-24 shows 0 only because the frozen corpora predate it. A new test keeps a carrier profile for every entity of the comm, identity, commerce, devops, ci, vcs and registry families.entity_map.yamlmaps the TruffleHog detectors that report these formats (PyPI,RubyGems,TerraformCloudPersonalToken,Pulumi,Doppler,Prefect,SnykKey,Sourcegraph,DroneCI,TravisCI;check-entity-mappasses on the pinned binaries).run_baselines.pycuts the cookie name from gitleaksgitlab-session-cookiefindings (stepcookie_name), whose regex has no secret group. Detection change: none (evaluation data only, no latency effect); the frozen corpora change only when the operator re-freezes them. Dev rows, exact F1: ARK 0 on all 20 (no rule; generic rules find up to 34 of 40 values); best baseline 100 PyPI (all three tools), Terraform Cloud (gitleaks, TruffleHog), Doppler and Prefect (TruffleHog), 97.4 Vault (gitleaks), 96.1 Travis CI, 91.9 Drone CI (TruffleHog), 65.5 Infracost/Octopus/Sourcegraph, 58.0 Artifactory, 50.0 Clojars, Harness and the GitLab session cookie, 49.4 Pulumi/RubyGems (TruffleHog 50.0), 48.4 Snyk (TruffleHog), 45.7 Bitbucket, Codecov and Sonar (gitleaks). Baseline precision suffers mostly fromdoc_example/test_keyvalues. After a re-freeze ARK reports 284 hard-negative spans on these rows (dlp_generic_credential_assignmentanddlp_env_var_secreton example values behind a prefix such asCLOJARS_,pul-,squ_,sgp_,rubygems_; nolookalikehit) — findings for P05/P06. - Synthetic DLP gold for the 17 comm, identity and commerce entities without gold (GOLD-D2):
comm.slack.app_config_token,comm.webhook_url,comm.discord.api_credentials,comm.telegram.bot_token,comm.twilio.api_key,comm.sendgrid.api_key,comm.brevo.api_key,comm.mailgun.key,comm.mailchimp.api_key,comm.messagebird.api_token,comm.mattermost.access_token,comm.sendbird.access_token,comm.gitter.access_token,identity.okta.access_token,identity.authress.access_key,identity.onepassword.secret,commerce.shipping.api_token. ARK has no rule for them yet (scopebaseline_only, packages P08, P09, P11).dlp_synth/rules.jsongainsbaseline_entities: 25 generators in the format of each entity's gitleaks rule (source ingenerators.json; the Authress key ends in a base64 PKCS#8 Ed25519 key, the 1Password service account token isops_+ base64 of the account JSON) and one lookalike per entity (wrong prefix, length or character set, e.g. a Twilio account SIDAC…, a Mailgunpubkey-). The E04 carriers render them like the value rules, withrule_idnull, in every family: per split and entity 40 positives (20 DE, 20 EN) and 200 hard negatives, including the new variantlookalikeunder the neutral keycorrelation_id(under the entity's own key a wrong-shaped value is still a credential).entity_map.yamlmaps the TruffleHog detectors that report these formats (TelegramBotToken,SendGrid,Mailgun,Mailchimp,MessageBird,Gitter,SlackWebhook,MicrosoftTeamsWebhook,Okta,DuffelToken,Shippo…) and detect-secrets'SlackDetectorfor webhook URLs, so every tool gets a per-entity number (check-entity-mappasses on the pinned binaries). The synthetic manifest counts rows perentitiestoo;baseline_report.pyfiles missed gold of a baseline entity under its entity id. Detection change: none (evaluation data only); the frozen dev and holdout corpora are unchanged until the operator re-freezes them. Dev rows of the new entities, exact F1 (best baseline): ARK 0 on all 17 (generic rules find up to 34 of 40 values, attributed tocredential.*), TruffleHog 97.4 MessageBird, 96.1 Telegram, 66.7 SendGrid, 57.9 shipping, 50.0 Mailgun/Mailchimp, 49.4 Gitter, 43.5 webhook URLs; gitleaks 84.1 Okta, 71.4 1Password, 66.7 Twilio, 50.0 Slack app/config tokens, 49.4 Brevo/Authress, 45.7 Discord/Mattermost/Sendbird. Baseline precision suffers mostly fromdoc_example/test_keyvalues. After a re-freeze, ARK recall ofAPI_KEYandSECRET_TOKENdrops by the uncovered gold, ARK reports 230 hard-negative spans on these rows (generic rules on example values whose prefix or suffix hides the example run, such asSK+deadbeef…; nolookalikehit), anddlp_slack_tokenreports thexoxp-…part ofxoxe.xoxp-1-…configuration tokens (overlap, not exact) — findings for P08. - Synthetic gold for the VAT IDs of IE, LT, LU, LV, MT, NL, NO, PL, PT, RO, SE, SI, SK and UK
(GOLD-VAT2), the last implemented ids without gold: the E05 and VAT goldsets now generate every
id of the entity map, and
NOT_GENERATEDis gone.synth_goldset.py --goldset vat-2writes a third goldset (corpusark-synthetic-vat-2, familyark_authored, own public dev seed, secret holdout seed and directorysynthetic-vat-2);ark-synthetic-de-euandark-synthetic-vat-1stay byte-identical (pinned digests unchanged). It has the same layout as part 1: per id and language one letter, form, JSON and CSV document with two checksum-valid numbers in the printed forms of the ARK rules, also the other issued forms (twelve-digit LT, the NL sole-trader btw-id with MOD 97-10, PT and SE natural persons, the IE second-letter and old forms, GB branches andXI), and the hard negativesvat_check_digit,vat_lengthandvat_postcode; UK postcodes are goldpii.uk.postcodeas in E05 instead of a look-alike. No hard negative of part 2 takes a value that another L1 rule reports by chance (IBAN mod 97, Luhn card number, German passport or identity card check digit), and a wrong NL mod 11 check digit never lands on a valid MOD 97-10 id. Dev: 112 rows, 16 positives per id; holdout: 224 rows. The runner freezes it once (gate stepsynthetic-goldset-vat-2, after the runner is reinstalled). Detection change: none (evaluation data only; Rust and every other goldset are unchanged). On the new dev set and ten random holdout seedsbalancedreaches exact F1 100 for all 14 ids with no hard-negative hit;strictreports none of them (scored-only rules). One holdout seed of ten also showed a GB branch VAT ID that is a Luhn-valid Maestro-shaped number reported as a card as well; that gold stays (a real ARK ambiguity, not filtered out of the data). - Synthetic gold for the VAT IDs of AT, BE, BG, CH, CY, CZ, DK, EE, ES, FI, FR, GR, HR and HU
(GOLD-VAT1). These ids were implemented (B01, B02) but had gold in no source family. The E05
generator
scripts/l1_presidio/synth_goldset.pygains--goldset vat-1: a goldset of its own (corpusark-synthetic-vat-1, familyark_authored, own public dev seed and secret holdout seed, own directorysynthetic-vat-1) so that the frozenark-synthetic-de-euset stays byte-identical. Per id and language one letter, form, JSON and CSV document carry two checksum-valid numbers in the printed forms of the ARK rules (BG, CZ and ES also numbers of natural persons) and the hard negativesvat_check_digit(wrong check character),vat_length(one digit outside the issued lengths) andvat_postcode(DK-8000); dev has 112 rows with 16 positives per id, holdout 224 rows. The tests check every positive and hard negative against the scoreboard's ports of the Rust validators and pin the dev output. The runner freezes the goldset once like E05 (gate stepsynthetic-goldset-vat-1, after the runner is reinstalled). Detection change: none (evaluation data only; Rust, the E05 set and every other goldset are unchanged). On the new dev setbalancedreaches exact F1 100 for all 14 ids with no hard-negative hit;strictreports none of them (scored-only rules, as decided in B01). -
German health care check digits (B-DE1: F10, F11, F12). The field-bound rules
pii_health_insurance_number_de,pii_rentenversicherung_deandpii_physician_number_lanr_denow compute the check digit instead of the form only:validators::de_health_insurance_number(KVNR, § 290 SGB V: the letter as 01…26 and eight digits, cross sums weighted 1-2, mod 10; not nine zeros),de_social_security_number(RVNR, VKVV § 4: birth day 01–31 or 51–81, month 01–12, the letter as 01…26, cross sums weighted 2-1-2-5-7-1-2-1-2-1-2-1, mod 10) andlanr(KBV: six digits weighted 4-9, the seventh digit completes the sum to ten; not one repeated digit). Their base points drop by the 20 points the check digit adds, sobalanced/recallscores stay the same. Inbalanced/recallpii_rentenversicherung_de_context_freereports a bare RVNR (15070649C103,15 070649 C 103) andpii_health_insurance_number_de_context_freea bare uppercase KVNR (A123456780, silent under a field that names another value, like the bare document numbers) at 0.55 (K05); nine bare digits pass the LANR check one time in ten, sopii_physician_number_lanr_de_proximityneeds a LANR term near the value or as its key (Arztnummer 987654401 auf dem Rezept,{"arztnummer": …}). New labelFACILITY_NUMBER_BSNR:pii_facility_number_bsnr_dereports nine digits with a BSNR term or key (BSNR,Betriebsstättennummer,practice number; lexiconpii.facility_number) in every mode; the BSNR has no check digit and is no M3 unit.uk_passport_numberbecomesnine_digits, shared by the UK passport and the BSNR.pii.de.bsnrbecomesshared; M3 count (count_validated.py) ARK 42 → 45, shared with Presidio 13 → 16. The invalid KVNRX123456789indynamic_pii.jsonl(L3) is marked"checksum_valid": false. Detection change: in every mode a KVNR, RVNR or LANR after its field name with a wrong check digit (or an impossible RVNR birth date, a LANR of one repeated digit) is no longer reported, and a BSNR with its term or key is a new finding; inbalanced/recallthe bare KVNR and RVNR and the LANR near its term are new findings. All 140 dev gold values of the three IDs pass the check digits. Dev scoreboard vs. 2f019e4 (PASS in strict and balanced, no new hard-negative FP, precision 100.0 on all four IDs): L1 F1 strict +0.8, balanced +1.1 pp (DE +2.5/+3.2, presidio_favored +0.5/+1.2, neutral ±0.0); balanced F1 RVNR 79.1 → 100.0, KVNR 93.5 → 99.1, LANR 92.3 → 96.3, BSNR 0.0 → 96.2. Latency (latency.py compare --engine ark-native, two base/head pairs): 100 KiB p95 135.8 → 145.0 ms (+6.8 %) and 145.2 → 145.3 ms (+0.1 %), both PASS; 4 KiB −7.6 % and −9.5 % (noise). Golden eval: 1201/1201 positives, 1113/1113 hard negatives. -
Australian and Singaporean identifiers (B-APAC: A08, A10), one label each with a check digit or letter:
AU_ABN(validators::au_abn, mod 89),AU_ACN(au_acn, weighted mod 10),AU_TFN(au_tfn, weighted mod 11),AU_MEDICARE(au_medicare, first digit 2-6, weights 1-3-7-9 mod 10),SG_NRIC_FIN(sg_nric_fin, check letter per seriesS T F G M, either case) andSG_UEN(sg_uen, check letter per format A, B with a registration year 1800-2099, C with an ACRA entity type).pii_abn_au,pii_acn_au,pii_tfn_au,pii_medicare_au,pii_nric_fin_sgandpii_uen_sgreport the value in every mode when a term of its label (ABN,Australian Company Number,TFN,Tax File Number,Medicare-Nummer,NRIC,FIN,UEN; new lexicon entriespii.au_*,pii.sg_*) stands near it or is its key, not as part of a longer number and not behind a nearer counter-context term. Inbalanced/recallthe forms whose shape adds to the check digit are reported bare at 0.55 (K05):pii_abn_au_context_free(2-3-3-3,51 824 753 556),pii_medicare_au_context_free(4-5-1,2123 45670 1),pii_nric_fin_sg_context_free(uppercase,S0642712Z) andpii_uen_sg_context_free(formats B and C,201434292D,S57TU0392K). ACN and TFN (nine digits pass mod 10/11 one time in ten/eleven), bare digit runs and a UEN of format A (eight digits and a letter, the form of a Spanish DNI) need the term in every mode, as the NHS number does.AU_ABNcompetes withSTEUERIDin the scored modes (K07: the same 2-3-3-3 grouping). The six IDs becomeshared; M3 count (count_validated.py) ARK 36 → 42.uk_nhs_numberand its scoreboard port share the new digit helper; validators are otherwise unchanged. Detection change: instrictthe six values with a term or key are new findings; inbalanced/recallalso the bare forms above. Dev scoreboard vs. 95a4edc (PASS in strict and balanced, no new hard-negative FP, precision 100.0 on all six IDs): L1 F1 strict +1.8, balanced +2.0 pp (presidio_favored +1.3, neutral ±0.0); balanced F1 ABN 100.0, ACN 82.6, TFN 91.9, Medicare 91.9, NRIC/FIN 95.7, UEN 96.3 (all 0.0 before). Latency (latency.py compare --engine ark-native): 100 KiB p95 146.9 → 143.4 ms (−2.4 %, PASS, noise); the regexes use ASCII boundaries, the context-bound rules reuse the K03 lexicon gate. Golden eval: 1160/1160 positives, 1060/1060 hard negatives;pii_l1.jsonlgains 124 rows. -
Detection change: (PII L1, all modes) the six AU/SG context-bound rules keep a value whose structural cell is exactly the value and whose key names the label, also next to numeric cells (
tfn,age\n876543210,42,uen,postcode\n53125226D,123456); before, the comma before a digit read as a thousands separator and dropped it. Outside such a cell a value still has to stand alone (PR #166 review). -
United Kingdom identifiers (B-UK: A07, baseline P6, candidate C4). New label
NHS_NUMBER:pii_nhs_number_ukreports ten digits with the NHS mod-11 check digit (943 476 5919,943-476-5919,9434765919;validators::uk_nhs_number, which also rejects check digit 10 and one repeated digit) in every mode when an NHS term (NHS number,NHS-Nummer,NHS) stands near the value or is its key; ten digits in phone groups pass mod 11 one time in eleven, so there is no context-free form.NHS_NUMBERcompetes withPHONEinbalanced/recall(K07), where the NHS term decides;strictkeeps both labels (T07).pii_ni_uknow reads a NINO in pairs (AB 12 34 56 C) and afterNI number,NI No.,National insurance noandNI-Nummer, in any case; its validatorvalidators::uk_ninois unchanged, so prefixes and letters HMRC never issues (BG,NT,FQ …,UV …) stay negative.pii_ni_uk_context_freereports a bare NINO in one letter case (AB123456C,hh 01 02 03 d,tw987654a;validators::uk_nino_context_free) inbalanced/recallonly, at 0.50.pii_passport_number_ukreports nine digits (925665416, HM Passport Office) next to a passport term or under a passport key in every mode.pii_driver_license_number_ukbinds a UK driving licence (MORGA657054SM9IJ: surname, birth date with month + 50 for women, initials;validators::uk_driver_license_number, format only) to the shared licence field part in every mode, andpii_driver_license_number_uk_context_freereports it bare inbalanced/recallat 0.50. NINO and licence join the grouped SSN as format-onlyscored_onlyrules: their issuing rules stand in for a check digit. The lexicon gainspii.nhs_numberand NI terms. M3 count (count_validated.py) ARK 35 → 36 (pii.uk.nhs_number, nowshared). Detection change: instrictNHS numbers with a term, NINOs in pairs or after the new field names, UK passports with a term and UK licences after a licence field name are new findings; inbalanced/recallalso bare NINOs and UK licences. Dev scoreboard vs. base (PASS in strict and balanced, no new hard-negative FP): balanced L1 F1 +1.6 pp (neutral +0.3, presidio_favored +1.4); driver's licence presidio_favored balanced 62.5 → 95.2 (Presidio tuned 90.0); NINO presidio_favored balanced 0.0 → 66.7 exact, 100.0 overlap (tuned 100.0: its gold spanYZ 61 48 68 Bkeeps the leading space of Presidio's regex, baseline P10); NHS number all families 0.0 → 94.0; phone balanced +2.0 (synthetic NHS values no longer read as phones). The new regexes use ASCII boundaries and stay on the DFA;latency.py compare --engine ark-native100 KiB p95 134.0 → 129.0 ms (−3.7 %, PASS; p50 32.0 → 35.3 ms, noise). Golden eval: 1091/1091 positives, 996/996 hard negatives;pii_l1.jsonlgains 128 rows and the known gapNI number: JK 55 66 77 Bcloses. -
UK passport numbers are nine digits (B-UK review, PR #165):
pii_passport_number_ukandvalidators::nine_digitsnow read nine digits as HM Passport Office numbers the 2015 and later designs (Her UK passport 123456789 expires in 2031.) instead of two letters and seven digits, and reject one repeated digit. The two-letter form moves to its own rule,pii_passport_number_two_letters(validators::passport_number_two_letters; Irish and Japanese passports such asPA1234567,TK1234567), so the dev scoreboard keeps its passport recall. Detection change: instrict,balancedandrecallnine digits next to a passport term or under a passport key are UK passport findings; two letters and seven digits are reported as before, underpii_passport_number_two_letters. -
US Social Security Numbers beyond the field name (B-SSN: T25, F07; baseline P1). Two rules join the field-bound
pii_ssn_usinark-core/src/detectors/pii/us.rs.pii_ssn_us_proximityreportsAAA-GG-SSSS(hyphens, dots or spaces) and nine digits without separators in every mode when an SSN term stands near the value (Social Security Number (SSN) 861-18-8883,… Social Security Number (800-40-3880),Als SSN ist … hinterlegt,536-90-4421 is my SSN) or is its structural key ("ssn": …,ssn=…,<SSN>, a CSV column);pii_ssn_us_context_freereports the grouped form without a term inbalancedandrecallonly (score 0.50, as presidio-analyzer reports it context-free). It is a format-onlyscored_onlyrule: the separators and SSA ranges stand in for a check digit, and without a termvalidators::us_ssn_context_freealso rejects the placeholders presidio-analyzer rejects in any context (one repeated digit as in111-11-1111, the counting sequence123-45-6789). A nearer counter-context term (order, reference, invoice, tracking, part or serial number) suppresses both in every mode.context::stands_aloneaccepts a+after a letter or another+as an EDIFACT element separator (NAD+SU+341-33-4328,LIN+1++…); a+that opens a number still rejects the value. Detection change: instrictgrouped values and nine digits with an SSN term or key are new findings (score 1.0), a grouped value without an SSN term stays silent; inbalanced/recallgrouped values without a term are new findings at 0.50, with a term 0.65–0.75. The SSA range validatorvalidators::us_ssnis unchanged, so the presidio-analyzer values987-65-4322and219-09-9999and nine bare digits without a term (078051121) stay misses. Dev scoreboard vs. base (PASS in strict and balanced, no new hard-negative FP, no other entity changes): US SSN F1 all families strict 64.2 → 70.9, balanced 64.2 → 81.3; neutral (Gretel) strict 43.3 → 50.8, balanced 43.3 → 68.5 (tuned 66.7); presidio_favored balanced 57.1 → 75.0 (tuned 88.9, capped at 75.0 by the unchanged validator); ark_authored 90.6 → 98.2 strict, 100.0 balanced. L1 F1 balanced +0.3 pp (neutral +0.4, presidio_favored +0.7). The golden known gapDie SSN des US-Mitarbeiters ist …closes;pii_l1.jsonlgains 68 rows. Both regexes use ASCII boundaries and digits and stay on the DFA: 0.10–0.16 ms per rule on the 100 KiB workloads; best-of-15 full PII scans base → head DE strict 119.8 → 114.8 ms, EN strict 26.0 → 24.8 ms (noise);latency.py compare --engine ark-native100 KiB p95 133.3 → 145.9 ms (+9.5 %, PASS; p50 38.0 → 36.8 ms). pii_ssn_us_proximitynow also requires the value to stand alone (context::stands_alone, as the context-free rule): a fragment of a longer number, range or phone number near an SSN term (My SSN is 401127789-0,Als SSN ist 422-68-7360-1 hinterlegt,+401127789) is no SSN. Detection change: all modes; such fragments are no longer reported.- Add connection-string passwords per scheme and LDAP bind passwords (G01). The new rule
dlp_connection_string_password(CREDENTIAL, subtypeconnection_string,structurestrength) reports thepassword/pwdvalue of a;-separated key/value connection string with a server, data source, address, host, DSN, driver or provider key or after ajdbc:URL (ADO.NET, ODBC with{…}values, OLE DB, JDBC SQL Server/DB2 properties), of a libpq keyword/value string with a host and a database, user or port, thepassword/pwdquery parameter of a database or broker URL (JDBC MySQL/PostgreSQL/MariaDB, libpq URI, SQLAlchemydialect+driver,&in XML) and the password of an Oracle thin/OCI URL (connection::is_connection_string). A bare value runs to the;, a quote, the line end or the next libpqkey=and may hold spaces. The new ruledlp_ldap_bind_password(CREDENTIAL, subtypepassword) reports the bind password of the OpenLDAP client tools (ldapsearch … -w),bindpw/bind_pw/bindCredentialkeys, SSSDldap_default_authtokand ApacheAuthLDAPBindPassword. Both drop references, placeholders, stand-ins and credential names, also in leetspeak (likeness::is_connection_password). Detection change: the subtypeconnection_stringis no longer planned; a connection-string password thatdlp_password_assignmentordlp_credential_in_urlreported before is now reported bydlp_connection_string_passwordwith the same span (the old matches stay inmatched_ruleswithsuppressed_by), the span of a query password in XML ends before&instead of including it, and ODBC passwords in braces, Oracle thin passwords, LDAP bind passwords and Redis passwords without a username (redis://:…@host,dlp_url_userinfo_password) are new findings. The placeholder check (T07) also reads a value that a generic rule took with the opening brace of an ODBC value, soPwd={<your-password>};is no longer reported bydlp_password_assignment. New entitiescredential.connection_stringandcredential.ldap_bind_passwordinresearch/dlp/entity_map.yaml, format sources inrule_sources.yaml, carrier profiles and probes indlp_synth.dlp_l1.jsonlgains 32 rows (18 positives, 14 hard negatives, DE and EN, in env, JSON, XML, YAML, INI, code, log, CLI and prose carriers); the newrust/tests/dlp_connection_strings.rschecks every scheme and the hard negatives. Scoreboard gate on the dev corpora PASS with unchanged numbers: exact F1 87.2, FP/MB benign code 0.1324 (5 FP), benign docs 0.1327, hard-negative FP 154; 100 KiB p95 +1.1 % and +5.5 % in two runs (11.23 → 11.35 ms, 10.35 → 10.93 ms; the base is above the 10 ms budget in both). Golden eval: 521/521 positives, 574/574 hard negatives. Existing validators are unchanged. - Detection change: (DLP L1, all modes)
dlp_connection_string_passwordanddlp_ldap_bind_passwordreport a quoted or ODBC-braced password that reads like code (bindpw "Violet.River!82"). Before, the member-access check rejected these values. A quote that closes a string still leads into code ("…;Password=" + cfg.DbPassword + "…"), and that case is now dropped. A bare value after a;runs to the next;even past akey=(Server=db;Password=Kx7pQ2vL9! part=Wm3zR8tQ4yN1;). It is no longer cut at the libpq key. - Add VAT identification numbers for LU, LV, MT, PL, PT, RO, SE, SI, SK, NO and GB (B16), one label
and one check-digit validator per country in
ark-core/src/detectors/pii/vat.rs, built like B01:VAT_ID_LU(first six mod 89),VAT_ID_LV(legal entities, weighted mod 11),VAT_ID_MT(weighted mod 37),VAT_ID_PL(NIP, weighted mod 11),VAT_ID_PT(NIF, weighted mod 11),VAT_ID_RO(CUI, weighted mod 11, six to ten digits),VAT_ID_SE(organisation number with Luhn plus01),VAT_ID_SI(weighted mod 11),VAT_ID_SK(mod 11),VAT_ID_NO(organisation number, weighted mod 11, withMVAin the span) andVAT_ID_GB(weighted mod 97 including the 9755 scheme, optional three-digit branch). Northern Ireland numbers (XI …) shareVAT_ID_GBand its validator. Forms without a confirmed check digit stay out: Latvian personal codes, Slovak birth numbers, Romanian CNPs and the UKGD/HAnumbers. The rules are scored-only like K05: an upper-case prefix and the national printed form report without a field name at 0.55, 0.80 with a term ofpii.vat_identifier, and drop below both thresholds behind a nearer counter-context term.VAT_ID_LVjoins the K07 competing labels, soLV 71890053205is one Latvian VAT ID and not also a bare Steuer-ID.entity_map.yamlclassifies the eleven validators as checksums (M3: ARK 24 → 35 units, GB and XI one unitpii.uk.vat_id) and adds elevenark_onlyIDs;scoreboard.pyports the validators; the frozen E05 synthetic goldset stays byte-identical and lists the new IDs as not generated.pii_l1.jsonlgains 220 balanced/recall rows (5 positives and 5 hard negatives per label in DE and EN). Detection change: none in the defaultstrictmode. InbalancedandrecallVAT IDs of the eleven countries are new findings;13_l1_golden_eval996/996 positives, 893/893 hard negatives. Existing validators are unchanged.
[0.1.11] - 2026-10-23¶
Detection changes in the default modes¶
The default PII and DLP mode is strict. These entries change what strict reports; the
detailed entries below carry the measurements. The scored modes balanced and recall, the
DLP output modes and the new labels that only they report are opt-in.
- Payment cards (B-PAN): a card number needs the IIN prefix and a length of its brand
besides Luhn. Card findings on phone numbers, EDIFACT/SWIFT stamps, token segments, ZA/KR IDs,
digits after
+and brandless Luhn-valid numbers disappear; CVV and expiry need a reported card, and a CVV in a CVV-headed table column is a new finding. - IP addresses (B-IP): spans include the zone ID and the CIDR prefix length, IPv4-mapped and embedded IPv6 addresses are one span, and IPv6 findings inside SWIFT MT/EDIFACT chains and on invalid mixed forms disappear.
- Phone numbers (T04, T17, T18): spans end at line breaks, NANP trunk and
00prefixes and a US area code in parentheses are part of the span, and EDIFACT/X12/SWIFT elements, zero-padded IDs and 6-4 digit groups are no phone numbers. - FR/ES/IT/NL field names (T26): date of birth, customer and employee ID, account number, CVV, US SSN, passport number and username are new findings behind these field names.
- Obfuscation (K06): values with zero-width characters, full-width forms or defanged separators are found in every mode.
- Secondary evidence (T07): a card or account finding inside a valid IBAN moves to
details.secondary_evidence; no finding is added or removed. - Combined DLP+PII scans (K08): PII spans that overlap a DLP secret span are dropped from the PII evidence.
- Provider tokens (F03, F04, F05, T01, K02): GitHub tokens need their exact length and, for classic tokens, a valid CRC32; npm tokens need a valid CRC32; all documented GitLab token prefixes are found; AWS access key IDs need a credential prefix and 16 Base32 characters, and AWS secret access keys are found by their structural context; prefix rules match whole tokens only; provider, key and JWT values below 3.0 bits of entropy per character are dropped.
- Crypto keys (F01, F02):
0xplus 64 hex digits is a private key only in a key context; WIF keys need a valid Base58Check, and testnet keys are found. - Generic credentials (F-GEN1, F-GEN2, T06, T07, T09, K05, K06, K08): far fewer findings on
code expressions, prose, placeholders, documented example values, UUIDs, digests and other
non-secret value shapes; new findings under compound, camelCase, quoted and typed keys, for
quoted passphrases and for unquoted config values with
;,,,}or]; tighter spans without quotes and one span per overlapping secret. - HTTP auth (F-HTTP):
BasicandBearercredentials and server session cookies are found outside a literal header line when the value proves its kind. - Shell commands (#128):
shell_commandscores with the A1 model (character n-grams plus shell tokens with pipe, redirect and command markers):curl … | jqand similar pipes into read-only filters are no longer flagged, loopback reverse shells are. The model asset revision changes, so warmup downloads A1;format_version: 1model files no longer load. - Credential phishing (#60): a request to verify identity or credentials at a login or auth link is a terminal injection finding on L1.
Result shapes only grow: dlp_mode and the PII modes are new execution gates, matched_rules
entries gain subtype, score, context, overlap, suppressed_by and structure, and PII
results gain details.secondary_evidence.
The Ark API scan config gains an optional dynamic_pii block (#146). Rust API: the
Ark-API-only SecurityGateway::enqueue_ark_api_categories_with_options and
enqueue_ark_api_categories_with_chunk_overlap_options are replaced by
enqueue_ark_api_categories_with_request_options.
Changed¶
- Score
shell_commandwith the A1cat-shell-risk-commandermodel (#128). A1 (format_version: 2) adds a second TF-IDF block over a deterministic shell tokenizer (shell_tokens, a port of the reference analyzer) with command, pipe and redirect markers (CMD=,PIPE->jqvs.PIPE->sh,REDIR->) next to the character n-grams; the asset revision moves to the A1 commit, so warmup downloads it. The loader readsformat_version: 2only; theformat_version: 1path, its parity fixture and generator are removed. A flagged command whose score comes from the shell tokens alone reports the trimmed command as its evidence span. Parity with scikit-learn 1.8 is below 1e-14 on the synthetic and the published model. Detection change:shell_commandscores change in every mode with the new model; thecurl … | jqfalse positive and the loopback reverse-shell gap of #128 close. - AWS access key IDs and HTTP Basic credentials (F05, F-HTTP, PR #142 review). Detection
change: legacy alphanumeric AWS access key IDs (16 upper-case letters or digits after
AKIA,ASIA,ABIA,ACCAorA3T…, also holding 0, 1, 8 or 9) are found again, also as the anchor ofdlp_aws_secret_key.dlp_basic_authreports Basic credentials with an empty side (token:,:token, RFC 7617) when the populated side is a machine-issued token and no placeholder; both sides empty stays silent. -
IP address spans (B-IP: T20 + baseline C1, TRIAGE 2026-W43 bucket 5, baseline P3/P9). An IPv4-mapped or embedded IPv6 address is one span (
::ffff:10.0.0.1,64:ff9b::198.51.100.1), the zone ID and the CIDR prefix length are part of the span (fe80::1%eth0,192.168.1.0/24,2001:db8::%eth0/128); a prefix length out of range stays outside it (10.0.0.0/123). The IPv6 rules no longer start inside a colon-delimited SWIFT MT or EDIFACT chain (:28C::52D:,:60F::61F::62F:,BGM+380:21:3::90:FRET,LIN+1::1:550:::EA:20), after.,-,+or a word character, and no longer report a value that a further:/.group continues, so an invalid mixed address (2001:db8::256.1.1.1) yields no sub-span; a SWIFT MT tag at the start of a line (:25:,:52A:) and a field name (IPv6:) open a value.validators::ip_addresschecks the prefix length and zone ID and also rejects loopback, unspecified and multicast IPv4 inside a mapped address (::ffff:127.0.0.1, D-IP); the network-diagnostic filter strips zone ID and prefix and checks the mapped IPv4. Gold convention inresearch/l1-pii/README.md. Detection change: instrictandbalancedIP spans grow by their zone ID and prefix length, mapped/embedded addresses replace their IPv4 sub-span, and IPv6 findings in SWIFT/EDIFACT chains and partial matches on invalid forms disappear. Dev: Gretel IP F1 90.6 → 96.7 (11 FP fewer, two SWIFT-tagged gold values now exact), presidio-analyzer IP tests 50.0 → 85.7, the P9 hard negative is no match. -
Payment card numbers need the IIN prefix and a length of their brand besides Luhn (B-PAN: T19 + F02, TRIAGE 2026-W43 bucket 4, baseline N1/P8).
pii_credit_cardvalidates with the newvalidators::payment_card: Luhn (unchanged), thenvalidators::CARD_IIN_RANGES(Amex, Diners Club, Discover, JCB, Maestro, Mastercard incl. 2221–2720, Mir, UnionPay, UATP, Visa 13/16/19 digits); 13 digits that open with a valid YYMMDD birth date (ZA ID number, KR resident registration number) and digits that open with a YYYYMMDD date (time stamps) are no card. The rule also no longer reports the digits right after+(+44 20 1234 5678, no sub-span either) or a-/_segment of a token (xoxb-…-485275870833-…).checksums.arkinentity_map.yamllistspayment_cardinstead ofluhn;scoreboard.pyports it for the gold-gap report. Detection change: instrictandbalanced(the same table gates the context-free card ofbalanced) card findings on phone numbers, EDIFACT/SWIFT stamps, token segments, ZA/KR IDs, IBAN BBANs without a card IIN and other Luhn-valid numbers without a brand disappear, and with them the card's secondary evidence inside such IBANs and itsoverlapentries next to phone numbers. Dev: card FP 95 → 11 (strict) and 59 → 8 (balanced), every card TP and every Luhn-valid card gold value stays found; the presidio-analyzer hard negative1748503543012is no match. - B-PAN follow-ups: CVV and card expiry now depend on a card that
pii_credit_cardreports (payment_cardplus the position check,context::has_payment_card), not on any Luhn-valid number, and a-/_-joined card-field label of the card lexicon (card-,card-number-,kartennummer-,karten-nr-) is no token prefix, so the PAN after it stays found. Detection change: instrictandbalanced, CVV and expiry findings next to a phone number or a brandless Luhn-valid number disappear, and compact or hyphen-grouped PANs after a hyphenated card-field label are found again. -
A CVV in a table column headed by a CVV label (
cvc,cvv, … as a CSV or Markdown-table header) is reported without a card in the text; aCVV: 123field or acvvkey elsewhere still needs a reported card. Detection change: instrictandbalanced, such column CVVs next to a phone number (found before only because the phone number passed Luhn) stay found, and column CVVs without any card number are new findings. -
Split the PII L1 rules out of
rust/src/detectors/pii/pii.rsinto regional modules (K01):common(e-mail, IP, MAC, international phone),intl(IBAN, card number, CVV, expiry, SWIFT/BIC),identifiers(field-bound employee, customer, patient, student and applicant IDs, username, date of birth, account number),de,usanduk; the Dynamic-PII field anchors move tocontext_anchors.rs. Each rule is aconstin its module, andpii.rskeepsPiiPattern, the pipeline and thePII_PATTERNSregistry, which lists the 39 rules in the previous order because the first match setsclass_nameand wins same-label overlaps.rust/tests/pii_registry_order.rspins that order, andparse_native_patternsinscripts/generate_docs.py(used by the rule catalog,entity_map.pyandcount_validated.py) resolvesmodule::CONSTentries. Rule ids, patterns, validators, goldens,13_l1_golden_eval, the generated docs and the ARK predictions on every dev corpus are bit-identical. Detection change: none; this is a refactor only. -
Move the PII L1 goldens (
pii_l1.jsonl) to golden schema v2. Every row names thepii_*rule it exercises (rule_id), its carrier document (carrier:line,prose,json,yaml,xml,csv,kv,table), difficulty tags (no_context,structured,obfuscated,ocr) and whether it is aknown_gap, a positive the detector misses today. The set grows from 140 to 713 rows: at least 5 gated positives and 5 hard negatives per label and language (DE/EN), every one of the 37pii_*rules with a gated positive in both languages and a hard negative, and 89 known gaps (quoted JSON keys, CSV headers, table and XML cells, Germanist/lautetafter the field name, zero-width and full-width characters, parenthesised US area codes). The 140 existing rows keep their ids, text and spans.13_l1_golden_evalnow checks PII coverage againstPII_PATTERNS: it requires that the declared rule reported each gold span, prints coverage perrule_id(DE | EN) and per tag and language, and fails if a PII rule loses its last passing positive in a language or a known gap starts passing.python/tests/test_pii_golden_schema.pyandrust/tests/pii_golden_schema.rscheck the schema statically. Detection change: none; this is evaluation tooling only. -
The DLP rules moved out of
rust/src/detectors/dlp/dlp.rsinto family modules underrust/src/detectors/dlp/rules/(providers,crypto,credentials,hashes,identifiers,content), each with its own validators.DLP_PATTERNSkeeps its path and type and concatenates the families in the previous rule order. That order is pinned inrust/tests/dlp_rule_registry.rsbecause the first matching rule sets the result class.scripts/generate_docs.pyandscripts/measure_demo_pii_dlp.pynow read the rule modules. Detection change: none. Goldens, the golden eval and the full DLP output on the dev split are bit-identical.
Added¶
- Driver's licence numbers for Germany, the US states and Italy (F15, baseline P6). The German rule
pii_driver_license_number_demoves torust/src/detectors/pii/driver_license.rsand two rules join it:pii_driver_license_number_us(a state format,validators::us_driver_license_number: the letter/digit runs of presidio-analyzer's US recognizer from one letter and four digits up, six to fourteen digits, WashingtonWDL…; digit groups may be joined by hyphens, a hyphenated calendar date is rejected) andpii_driver_license_number_it(patenteRM5512345Kor paper licenceU1H34K56LX,validators::it_driver_license_number). All three share one field part: a licence field name in DE, EN, IT, FR, ES or NL (Führerschein-Nr.,Fahrerlaubnisnummer,driving licence no.,DL number,numero di patente,patente di guida,permis de conduire,licencia de conducir,rijbewijsnummer) before the value or as its structural key, with prose such as "my driver's license number is", "of", "lautet" or "è" in between. Thepii.driver_licenselexicon gains the curly-apostrophe andDLterms and FR/ES/IT/NL lists. None of the formats has a check digit (entity_map.yaml: format only, M3 unchanged). Detection change: in every mode licence numbers after a licence field name are new findings (strict scores 1.0, balanced 0.65 US, 0.70 IT, 0.75 DE), also a US or Italian value after a German field name; a number without a licence field name stays silent in every mode, so the bare presidio-analyzer values (H12234567,KO12345678X) remain misses. Dev scoreboard vs. base (PASS, no new hard-negative FP): driver's licence F1 neutral 0.0 → 37.1 (tuned 6.2), presidio_favored 0.0 → 52.6 (tuned 78.3; the gap is the five UK licences of B-UK, without them 71.4 vs. 61.5), ark_authored 41.4 → 72.2, identical in strict and balanced; L1 F1 +0.7 pp in both modes.12_l1_pii_fixture_evalcloses its US licence miss (F1 0.9857 → 0.9929). The golden negativeDriver's license number: D123456789was a valid US format and becomes a positive (pii_l1keeps its id with a letters-only value); the two known gaps… lautet J0101234K57andDriving licence no.: …close.pii_l1.jsonlgains 53 rows (US, IT incl. Italian, FR/ES/NL). Existing validators are unchanged. The field part uses ASCII word boundaries so the regexes keep the DFA on German text: 0.18 ms per rule on the 100 KiB DE workload (the old DE rule 0.3 ms);latency.py compareark-native 100 KiB p95 149.3 → 138.3 ms (PASS, noise). validators::us_driver_license_numberalso rejects a month-day-year date (04-30-2031,04-30-31), which it took before as six to fourteen digits once the hyphens were removed. Detection change: in every mode a US-style expiry date after a licence field name is no longer reported as a US licence number.- Pass the dynamic PII labels per scan request (#146). The Ark API scan
configtakes an optionaldynamic_piiblock with the shape ofpipeline.dynamic_pii—labels(required),threshold,label_thresholds,execution_gateandconditional_labels— that replaces the worker's labels, thresholds and gates for that request only; input limits, chunking and timeouts stay those of the worker. The block needsdynamic-piiamong the request's categories, is validated like the server config plus a 64-byte limit per label, and an invalid block is rejected with422. The ARK Coordinator checks the block with the same rules before it accepts the job and forwards it to the worker. In Rust,DynamicPiiRequestConfig::resolvebuilds the request config from the gateway's andSecurityGateway::enqueue_ark_api_categories_with_request_optionsqueues a request with it; it replaces the Ark-API-onlyenqueue_ark_api_categories_with_optionsandenqueue_ark_api_categories_with_chunk_overlap_options, which are removed. Requests with different label lists never share results: the config travels with the request's dynamic PII job, and the GLiNER chunk cache keys include the inference labels. Detection change: none without the block; with it,dynamic-piireports the requested labels (for examplestreet_address) instead of the worker's defaults. -
Add the injection L1 governance rule
ark.injection.governance.credential_verification_linkfor credential phishing (ASI-09, #60). It reports a request to verify, confirm or re-enter an identity, account, credentials or password (DE:bestätigen/verifizieren…Identität,Konto,Zugangsdaten,Passwort,Anmeldedaten) followed in the same clause by an http(s) link with a login or auth host label or path segment (login,signin,auth…; query and fragment are not searched and stay outside the span). It is audited high precision and scores in theark.injection.agentic.control_abusefamily, the family of the existingphishingrelation, which alone stayed far below the acceptance threshold. An e-mail or order confirmation link, a sign-in link without a verification request and prose about phishing stay silent. The governance goldens gain 5 positives and 6 hard negatives (DE and EN), among them a link with a long token query, anauth_tokenquery parameter and an/authorpath. Detection change: such phishing requests are now terminalinjectionfindings on L1 (agentic_control_abuse); no other golden changes. -
Add ICAO 9303 check digits for German passport and identity card numbers (F08), one validator per document (weights 7-3-1,
A=10 …Z=35):validators::de_passport_number(seriesC F G H J K) andvalidators::de_identity_card_number(seriesL M N P R T V W X Y), each for eight characters of the ICAO alphabet plus the check digit (F12345671,L01X00T44, the form presidio-analyzer and its tests use), the MRZ form of the nine-character serial plus its check digit (C01X00T478,T220001293, also insideIDD<<T220001293<<…) and the old numeric serial of ten digits (1220001297). The four new rules are scored-only like K05:pii_passport_number_de_context_freeandpii_identity_card_number_de_context_freereport a letter-led value without a field name at 0.55, 0.80 with a document term (Pass-Nr., baseline P6,Reisepass-Nr.,Ausweis-Nr.,ID card no.;pass-nrwas already a K03 term ofpii.passport, the rule that reads it was missing), and drop below both thresholds behind a nearer counter-context term;…_legacyreports the old numeric serial only with a document term or key. A bare value of a field that names something else stays silent (DEA-Nummer: RN4135883,Codice Fiscale: …, adriver_license_numbercolumn), and inbalanced/recallpassport and identity card findings compete with an overlapping finding of any other label (overlap::DOCUMENT_LABELS): a field-bound value of another label wins, soKundennummer K12345676staysCUSTOMER_IDandPassport number: Y22400855stays a passport. The field-bound rulespii_passport_number_deandpii_identity_card_number_dekeep their format check (de_document_number) in every mode, since a printed nine-character serial carries no check digit.entity_map.yamlclassifies both validators as checksums with countryde(M3: ARK 11 → 13 units, 7 shared with Presidio);scoreboard.pyports them.pii_l1.jsonlgains 56 balanced/recall rows (at least 5 positives and 5 hard negatives per label in DE and EN). Detection change: none in the defaultstrictmode (dev scoreboard +0.0 pp everywhere). InbalancedandrecallGerman passport and identity card numbers are new findings without a field name: dev passport F1 38.5 → 44.9 (presidio_favored 0.0 → 34.3, ark_authored 70.3 → 76.9, neutral 38.0 → 37.7 with one new FP on an unannotated value), identity card F1 47.3 → 70.8, balanced L1 F1 +0.3 pp (DE +0.6 pp), no new hard-negative FP; 13_l1_golden_eval 734/734 positives, 638/638 hard negatives. Existing validators are unchanged. - The foreign-field guard of F08 also covers the old numeric serial (
pii_passport_number_de_legacy,pii_identity_card_number_de_legacy, PR #132 review): a key that names another value overrides a document term found by proximity, sopassport: archivedfollowed bymember_code: <ten digits>reports no passport. Detection change: none instrict; inbalancedandrecalla checksum-valid ten-digit value under an unrelated key no longer becomes a passport or identity card finding because a document term stands nearby. A document key (passport_no:,Ausweisnummer:) and a key that holds a document term itself (old passport number:, newcontext::key_holds_term) still report it; validators are unchanged. - Add VAT identification numbers for DE, AT, CH, FR, NL, BE, IT and ES (B01), one label and one
check-digit validator per country:
VAT_ID_DE(ISO 7064 MOD 11,10),VAT_ID_AT(UIDATU, Luhn variant),VAT_ID_CH(UIDCHE, weighted mod 11),VAT_ID_FR(key(12 + 3 × SIREN) mod 97plus SIREN Luhn),VAT_ID_NL(mod 11 or ISO 7064 MOD 97-10, branchB01–B99),VAT_ID_BE(97 − first eight mod 97),VAT_ID_IT(Partita IVA: Luhn plus province office code) andVAT_ID_ES(DNI/NIE control letter or CIF control character), inrust/src/detectors/pii/vat.rs. The rules are scored-only like K05: an upper-case country prefix and the national printed form (DE 136 695 976,CHE-100.155.212 MWSTwithout the suffix,FR 40 303 265 045,BE 0403.019.261) report without a field name at 0.55, 0.80 with a term of the new lexicon entrypii.vat_identifier(USt-IdNr.,UID-Nummer,MWST-Nr.,VAT ID,TVA,btw-nummer,Partita IVA,NIF-IVA…), and drop below both thresholds behind a nearer counter-context term. A bare Partita IVA (pii_vat_id_it_proximity, 11 digits withoutIT) needs such a term or key and joins the K07 competing labels, so it wins over a bare Steuer-ID on the same digits. A value that starts with a letter no longer counts as joined by,or a digit group before it (context::stands_alone, CSV cells); bare numeric values (Steuer-ID) are unchanged.entity_map.yamlclassifies the eight validators as checksums (M3: ARK 3 → 11 units), maps DE and IT assharedand the six others asark_onlyIDs;scoreboard.pyports the validators; the frozen E05 synthetic goldset stays byte-identical and lists the six new IDs as not generated.pii_l1.jsonlgains 214 balanced/recall rows (5 positives and 5 hard negatives per label in DE, EN and the national language). Detection change: none in the defaultstrictmode (dev scoreboard +0.0 pp everywhere). InbalancedandrecallVAT IDs are new findings: devpii.de.vat_idF1 98.9 (90/92 gold, 0 FP; the two misses are presidio-analyzer values with a failing check digit) andpii.it.vat_codeF1 96.4 (27/29, 0 FP), balanced L1 F1 +2.1 pp, no new hard-negative FP on any dev corpus; 13_l1_golden_eval 706/706 positives, 610/610 hard negatives. Validators of existing labels are unchanged. VAT_ID_FRjoins the K07 competing labels (B01 review): the eleven digits of a French VAT ID with a space afterFR(FR 40 303 265 045,FR 40303265045) can also pass the context-free Steuer-ID, which kept a second, falseSTEUERIDfinding next to the VAT ID. The VAT ID is the longer value and wins like a Partita IVA; the other VAT IDs of B01 have no eleven-digit body and no competing overlap. Validators are unchanged. Detection change: none instrict; inbalancedandrecallsuch a French VAT ID reports oneVAT_ID_FRfinding, and the Steuer-ID rule'smatched_rulesentry namesoverlap(longer_value,yields_to: VAT_ID_FR).- Add VAT identification numbers for BG, CY, CZ, DK, EE, EL, FI, HR, HU, IE and LT (B02, part 2
of B01), one label and one check-digit validator per country in
rust/src/detectors/pii/vat.rs:VAT_ID_BG(nine digits weights 1…8, fallback 3…10, mod 11; ten digits ЕГН with birth date, ЛНЧ or other person),VAT_ID_CY(check letter mod 26, never12…),VAT_ID_CZ(legal entity weights 8…2 mod 11 and never9…, special6…, ten-digit birth number with birth date and mod 11),VAT_ID_DK(CVR weights 2-7-6-5-4-3-2-1 mod 11),VAT_ID_EE(KMKR10…, weights 3-7-1 mod 10),VAT_ID_EL(prefixELorGR, powers of two mod 11),VAT_ID_FI(weights 7-9-10-5-8-4-2-1 mod 11),VAT_ID_HR(OIB, ISO 7064 MOD 11,10),VAT_ID_HU(weights 9-7-3-1 mod 10),VAT_ID_IE(check letter mod 23, second letter since 2013, old form) andVAT_ID_LT(nine or twelve digits with the1marker, weights 1…9 mod 11 with a second step). The algorithms follow python-stdnum 2.2, stricter where a form carries no check digit (nine-digit Czech birth numbers before 1954) or is no issued number (all-zero bodies, Estonian numbers outside10…, impossible Czech birth months). Like B01 the rules are scored-only (K05): an upper-case country prefix and the printed form (DK 13 58 56 28,CY-10259033P,HU-12892312) report without a field name at 0.55, 0.80 with a term ofpii.vat_identifier, which gains English country terms (Czech VAT number…) and national ones (ИН по ДДС,ΦΠΑ,DIČ,CVR-nr.,KMKR,ΑΦΜ,ALV-numero,OIB,PDV ID,közösségi adószám,PVM kodas…), and drop below both thresholds behind a nearer counter-context term.VAT_ID_HRandVAT_ID_LTjoin the K07 competing labels: the eleven digits ofHR 57419348408can also be a context-free Steuer-ID and the twelve digits ofLT 621223427710a card number; the VAT ID is the longer value and wins. The rules are appended toPII_PATTERNS, so the order of existing rules is unchanged.entity_map.yamladds elevenark_onlyIDs (Greece aspii.gr.vat_id) and classifies the validators as checksums (M3: ARK 13 → 24 units);scoreboard.pyports them; the frozen E05 synthetic goldset stays byte-identical and lists the new IDs as not generated.pii_l1.jsonlgains 242 balanced/recall rows (per label 6 DE and 5 EN positives, 6 DE and 5 EN hard negatives). Detection change: none in the defaultstrictmode. Inbalancedandrecallthe eleven VAT IDs are new findings, and a spaced Croatian or Lithuanian VAT ID no longer adds aSTEUERIDorCREDITCARDfinding on its digits (the rule'smatched_rulesentry namesoverlap,longer_value); 13_l1_golden_eval 855/855 positives, 759/759 hard negatives. Validators of existing labels are unchanged. VAT_ID_IEaccepts the old Irish form with+or*as the second character (IE8*79739I,IE8+79739I; B02 review): the pattern allowed only a letter there, and the validator dropped the symbol before the check. The check letter is unchanged (0, the five digits and the first digit, mod 23), andscoreboard.pyports the change. Detection change: none instrict; inbalancedandrecallsuch a legacy Irish VAT ID is a newVAT_ID_IEfinding.- Implement the decided M2 rule and add
scripts/l1_presidio/scoreboard.py m2(D-M2, decision of 2026-10-21 inresearch/l1-pii/README.md, M2 row and fairness section). M2 holds only if it holds on both milestone families on their own, judged onbalanced: macro-F1 is the unweighted mean exact F1 over thesharedandark_onlyids with gold on German rows of the family;neutralneeds Presidio tuned + 15 pp,presidio_favoredparity (tuned + 15 pp exceeds 100 there).presidio_onlyids stay out of the macro average (listed as excluded) untilentity_map.yamlmarks themshared; a family without such gold does not hold.m2takes--ark MODE=DIR(balanced required) and--presidio DIRlikem1, prints macro-F1, target and per-id F1 per family and mode, and exits 1 unless both family rules hold on balanced.reportadds an M2 table per Ark mode and Presidio profile. Detection change: none; this is evaluation tooling only. - Score the L1-PII scoreboard in the PII modes
strictandbalancedand report gold gaps of the frozen corpora separately (B-EVAL: T15 + T16, decisions of 2026-10-21 inresearch/l1-pii/README.md).scripts/l1_presidio/scoreboard.py gaterunsrun_ark.py --pii-modefor head and base in both modes (one build per side), checks that the runner confirms the mode, and fails on an F1 loss above 0.5 pp or a new hard-negative FP in either mode; stdout, the JSON report (modes.<mode>.<split>) and the per-commit results carry the mode, and strict predictions keep their cache layout. Milestones M1–M3 are judged onbalanced:m1andreporttake--ark MODE=DIR(balanced required), print every mode and read the verdict from balanced.reportadds a gold-gap section per corpus and entity for every engine: exact-span false positives whose value equals an annotated gold value of the same id elsewhere in the row, and gold values failing every checksum validator of their id (Python ports ofluhn,mod97,steuer_id,steuer_id_issuedinCHECKSUM_PORTS), with the number each engine still reports exactly; no metric changes.research/l1-pii/scoreboard/baseline.mdadds the balanced numbers on dev (neutral exact F1 61.8 balanced vs. 61.4 strict vs. 40.5 Presidio tuned; M1 on balanced 4/9 neutral, 5/12 presidio_favored) and the gold gaps (Gretel 21/53 PAN and 14/70 IBAN gold fail the checksum, Nano 65/82 PAN; 15 Ark and 17 Presidio e-mail FP equal an annotated value elsewhere in the row). Detection change: none; this is evaluation tooling and a report only. - Add DLP taxonomy v2 (K09). The entity groups stay unchanged; every credential/secret rule
names a subtype within its group (
RuleMeta::subtype,patronus_ark::detectors::dlp::dlp::SecretSubtype), and each of itsmatched_rulesentries reports it assubtypenext toprovider:api_key,access_token,secret_key,private_key,password,password_hash,generic_secret,session_token,signatureandauth_url(dlp_credential_in_url). Connection strings, webhook URLs, OTP secrets and seed phrases become the subtypesconnection_string(CREDENTIAL),webhook_url(SECRET_TOKEN),otp_secret(CREDENTIAL) andseed_phrase(CRYPTO_KEY) for the rules of G01, P08, G08 and C01 instead of new groups.research/dlp/entity_map.yamldrops the planned groupsCONNECTION_STRING,WEBHOOK_URL,OTP_SECRETandSEED_PHRASEand lists the subtypes; the gitleaks webhook rules map toSECRET_TOKEN. The result schema documents the taxonomy and the L1 rule catalog shows the subtype per rule. Detection change: none; spans, labels and scores are unchanged,matched_rulesentries gain thesubtypefield. - Add overlap resolution for DLP secret findings and DLP vs. PII double labels (K08). Overlapping
credential/secret spans keep one span: a span that only adds quotes or a trailing
;/,to a tighter span of the same tier gives way to it; a fixed-format match strictly inside a longer fixed-format value of another rule (a WIF-shaped Base58 run inside a Stripe key, OAuth token or JWT), or a keywordless match inside a longer secret value, is a substring artefact; then the tier decides (provider/key material > platform tokens > password hashes > structural credentials > generic assignments;patronus_ark::detectors::dlp::dlp::overlap_tier), within a tier the stronger validator, then the registry order. Suppressed matches stay inmatched_ruleswithsuppressed_by;run_ark.pyattributes a range to the rule that kept it. A generic key that is the userinfo of a URL (https://token:…@host) belongs to the URL credential. When one request scans DLP and PII on the same text, PII L1 spans overlapping a DLP secret span are dropped from the PII evidence (URL passwords read as e-mail local parts, Slack token digits read as card numbers); the PII decision stays. Detection change: fewer DLP spans (genericenv/assignment spans around provider tokens and JWTs, WIF artefacts inside other keys, the host after an empty URL password), tighter generic spans without quotes, URL passwords instead of the whole URL under atoken:userinfo, and fewer PII spans in combined DLP+PII scans.dlp_btc_wifnow validates Base58Check (mainnet version byte0x80, a secp256k1 scalar in range, the0x01flag for compressed keys); scoring stays at format strength. Detection change: WIF-shaped strings with an invalid checksum (documented example and test fills, random Base58 runs inside hashes, Stripe keys, OAuth tokens and JWTs) are noCRYPTO_KEYfindings. - Add a DLP rule prefilter (K07) so that the rule set scales to 200+ rules. One Aho-Corasick pass
finds the prefilter keywords of all rules (
RuleMeta::keywords, ASCII case-insensitive); a rule without a keyword in the text does not run. The keywordless rules get a structural prefilter (RuleMeta::value_run: 24 alphanumerics fordlp_discord_token, 51 Base58 characters fordlp_btc_wif). Rules whose matches start at most 64 bytes before a keyword and that cannot run on through a wide class are tried only at those positions, in text order, with an anchored probe; this takesdlp_generic_credential_assignment(Unicode\bon non-ASCII text) from about 6.5 ms to 0.2 ms per 100 KiB of code. Every gate is proven from the rule's regex syntax tree at pipeline construction (patronus_ark::detectors::dlp::prefilter); a rule without a proven gate runs on the whole text and failsrust/tests/dlp_rule_prefilter.rs, so new rules must declare keywords. Texts with LONG S or KELVIN SIGN, which(?i)matches forsandk, run every rule on the whole text. Native DLP time on 100 KiB benign code drops from about 9.4 ms to 3.2 ms (in-process, release build). Detection change: none; the matches equal a full scan on all goldens, the rule fixture and keyword-dense random texts. - Add a DLP score model and the DLP output modes
strict,balancedandrecall(K04), on the PII score model: every DLP rule declares a base (1–100 points for its regex form); a finding scores base plus validator strength (checksum 35, structure 25, format 15, none 0) plus its context. A rule that captures the value behind a field name of its own adds 25; a secret without a field name (provider tokens, wallet and PEM keys, JWTs) takes the nearest DE/EN term on its line with the PII proximity rules: a credential term ofanchor_lexicons/dlp.jsonadds 25/20/15, a nearer digest/hash/ID term (new lexicon entrydlp.digest_reference: checksum, tx hash, Transaktionshash, Prüfsumme, commit SHA, sha256, UUID …) subtracts 30. The mode is a new execution gate:ScanGateMatrix.dlp_mode(DlpMode), Pythonexecution_gates={"dlp_mode": …}and Ark APIgates.dlp_mode; unknown names are rejected.balanced(0.50) andrecall(0.30) keep thestrictspans that reach the threshold, after the same overlap resolution, and reportscoreandcontextinmatched_rules; a bare0x+ 64 hex value scores 0.40 and next totx hash0.10.run_ark.pytakes--dlp-mode, and the DLP scoreboard reports P/R curves (exact and overlap P/R/F1 and hard-negative FP per score threshold) from arecallrun on the carrier rows, without gating them. Detection change: none in the defaultstrictmode; goldens, 13_l1_golden_eval and the scoreboard profilesdefault/extendedare unchanged. Newdlp_l1goldens run inbalanced(dlp_mode), and the rule catalog lists base, context source and score range per DLP rule. -
Shared validator library
patronus_ark::detectors::validatorsfor DLP and PII rules: CRC-32 and the CRC32-Base62 suffix of GitHub/npm tokens, Base58/Base58Check, Bech32/Bech32m (BIP 173/350, configurable length limit, 5/8-bit regrouping), strict canonical base64 (standard, unpadded, URL-safe), DER TLV parsing with minimal-length and INTEGER checks, JWT header (alg,typ, unsecurednone) and compact JWS parsing, Shannon entropy per character, character classes and token alphabets. Spec test vectors (CRC-32 check value, Bitcoin Core base58, BIP 173/350, RFC 4648, X.690) plus a cross-check against the golden generator are inrust/tests/validators.rs. The DLP entropy floor now uses the sharedshannon_entropy. Detection change: none; no rule uses the new checks yet (F01–F09 build on them). -
DLP rule metadata v2: every rule in
DLP_PATTERNScarries aRuleMeta(provider,validator_strengthchecksum/structure/format/none, prefilterkeywords,min_entropy,source_ref, live/test severity viatest_prefixes). Eachmatched_rulesentry in the native DLP layer details now reportsprovider,validator_strength,source_refand, for credential/secret rules,severity(testfor Stripesk_test_/rk_test_keys, otherwiselive).research/dlp/rule_sources.yamllists the format source of every provider and key rule (gitleaks rule ID or vendor documentation; six legacy provider rules point to the F/T task that documents them), and the L1 rule catalog renders provider, validator strength and source.DlpPatterngains the publicmetafield. Detection change: provider, key and JWT rules drop values with a Shannon entropy below 3.0 bits per character (synthetic values drawn from a handful of characters); all golden positives and dev positives stay above the floor. Spans of all other matches are unchanged. -
scripts/l1_dlp/baseline_report.pyscores ARK, gitleaks, TruffleHog and detect-secrets on one frozen DLP split with the scoreboard metrics and prints the tables of the new E14 baseline reportresearch/dlp/scoreboard/baseline.md(untyped and per-group P/R/F1, hard-negative FP per variant, FP/MB, recall per secret entity) plus ARK error buckets per rule (hard-negative FP, missed and boundary gold spans, benign-code FP). The report adds the dev measurements, the latency comparison, a static coverage matrix against Google Cloud DLP and Microsoft Purview and TRIAGE candidates. Detection change: none; this is evaluation tooling and a report only. - Add FR/ES/IT/NL field names to the existing PII anchor rules (T26, TRIAGE 2026-W43 bucket 12). Gretel's 660 FR/ES/IT/NL rows hid many field-bound values behind field names that only DE/EN rules knew.
pii_date_of_birth(and its written-date variants) now acceptsdate de naissance,né(e) le,fecha de nacimiento,nacido/nacida el,data di nascita,nato/nata il,geboortedatum,geboren op;pii_customer_idnuméro/ID/identifiant de client,référence client,número/ID/código de cliente,codice/numero/ID cliente,klantnummer,klant-id;pii_employee_idnuméro/identifiant d'employé,número (de identificación) del empleado,ID/codice/matricola dipendente,personeelsnummer,medewerker-id;pii_financial_account_numbernuméro de compte (bancaire),número de cuenta,numero di conto,conto corrente,(bank)rekeningnummer;pii_credit_card_cvvcard-qualified security codes (code de sécurité de la carte,cryptogramme visuel,código de seguridad de la tarjeta,codice di sicurezza della carta,creditcard-beveiligingscode);pii_ssn_usnuméro de sécurité sociale,número de seguro social,numero di previdenza sociale,sociaal zekerheidsnummer;pii_passport_number_denuméro de passeport,número de pasaporte,numero di passaporto,paspoortnummer,reisdocumentnummer;pii_usernamenom d'utilisateur,nombre de usuario,nome utente,gebruikersnaam, only with:,=or#. The field names also bind as structural keys (K04). The K03 lexicon entries of these labels inanchor_lexicons/pii.jsongain optionalfr,es,itandnllists that the context engine and the lexical anchor scanner read.pii_l1.jsonlgains 64 rows (10 positives and 6 hard negatives each for FR, ES, IT, NL); the rule fixture probes the eight rules in the four languages. Detection change:strict(and the scored modes) report date of birth, customer and employee ID, account number, CVV, US SSN, passport number and username values behind these field names; dev corpora (strict, exact F1): Gretel L1 50.5 → 52.0 (date of birth 54.7 → 66.7, customer ID 37.5 → 46.7, passport 52.5 → 61.5, employee ID 75.0 → 79.0, account number 3.7 → 7.9, username 6.0 → 8.8), OpenPII Nano passport 3.7 → 10.7, synthetic DE/EU unchanged, no hard-negative FP. Generic security codes and usernames in prose stay silent. Validators are unchanged. - Add secondary evidence to the default
strictPII mode (T07, decision 2026-10-14, TRIAGE 2026-W42 bucket 4).strictkeeps emitting both labels bit-identically (spans, class and confidence unchanged), but aCREDITCARDorFINANCIAL_ACCOUNT_NUMBERfinding inside a reported, MOD-97-valid IBAN is now listed under the newdetails.secondary_evidence(label,start_byte,end_byte,reason:iban_valuefor the IBAN's own span such asBank account: GB82WEST12345698765432,iban_partfor a part such as the Luhn-valid BBAN ofDE18 4306 0967 1234 5678 00;primary_label:IBAN).overlap::SECONDARY_IN_IBANnames the two labels. A card or account next to an IBAN, and the digits of an IBAN form whose check digits fail, stay primary;balancedandrecalldrop these findings instead (K07) and list nothing. Evaluations score primary findings only:scripts/l1_presidio/run_ark.py(scoreboard input) drops secondary spans and counts them assecondary_spansin its sidecar,13_l1_golden_evalandpii_gold_hygiene.rsskip them, and apii_l1hard negative may annotate other real entities (n(also=…), never its target label) that must be the only primary findings.pii_l1gains 24 strict rows (6 card/account positives next to a valid IBAN and 6 card/account hard negatives inside one per language);l1_bilingual_rules.jsongains secondary probes forpii_credit_cardandpii_financial_account_number; the three bucket-4 entries leave the gold-hygiene detector-FP list. Detection change: no finding is added or removed in any mode; instrictthe PAN inside a valid IBAN and the account finding on an IBAN value are no longer scored. Scoreboard gate on the dev corpora PASS (no regression, no new hard-negative FP): card-number exact F1 42.5 → 42.8 (+0.3 pp), IBAN and account numbers unchanged, L1 F1 50.6 unchanged;13_l1_golden_eval488/488 positives, 390/390 hard negatives. Validators are unchanged. - Add cross-label overlap resolution to the scored PII modes (K07). Number labels compete for the same digits: the BBAN of a grouped IBAN or the digits of a
+49/+44number can pass Luhn, and an account field can hold an IBAN or a card-shaped number. Inbalancedandrecallthe newdetectors::pii::overlapresolves overlapping findings of two different labels amongCOMPETING_LABELS(IBAN,CREDITCARD,FINANCIAL_ACCOUNT_NUMBER,STEUERID,PHONE) to one; the first criterion that differs decides: an IBAN, the longer value, context that supports the finding's own label (field name, structural key or nearby support term:Telefon: 0151 2345 6789 01,Kontonummer: 4111-1111-1111-1111,via telephone at 0157-09-316-8811), a checksum, the higher score, the earlier rule. A finding inside a printed IBAN form (DE64 7172 7958 4924 3372 11,FR76 7014 4858 …) yields even when the IBAN's check digits fail or the grouped IBAN is not reported yet (T05). The loser leavesevidence_spans; itsmatched_rulesentries stay and carryoverlap(reason:iban,longer_value,context,checksum,score,rule_order,iban_form;yields_to: the winning label), andclass_name/confidencefollow the remaining findings. Other labels keep their overlaps (EMAILandUSERNAMEon one address), andstrictkeeps both labels (decision T07).pii_l1gains 24 balanced/recall rows (6 card positives next to an IBAN or phone with Luhn-valid digits and 6 IBAN-form hard negatives per language);l1_bilingual_rules.jsongains overlap probes for 5 rules. Detection change: none in the defaultstrictmode (scoreboard gate on the dev corpora PASS, +0.0 pp, no new hard-negative FP;13_l1_golden_eval476/476 positives, 378/378 hard negatives). Inbalancedandrecallcard numbers inside IBANs and phone numbers, and on account or phone fields, are no longer reported: on the dev corpora card-number exact F1 rises from 47.5 to 56.6 on Gretel Finance, 46.3 to 52.1 on the synthetic DE/EU set and 33.7 to 34.0 on OpenPII Nano, with card recall, IBAN, phone and account numbers unchanged (L1 F1 50.5 → 50.9 on Gretel, 48.1 → 48.3 synthetic). Validators are unchanged. - Add obfuscation normalization with offset back-mapping to the PII L1 detector (K06). The new
detectors::obfuscationbuilds a normalized view of a text with a byte map back to the original; when it undoes anything, the PII pipeline scans the view as well and adds every finding whose original span contains an undone obfuscation. In every mode it removes zero-width characters (U+200B–U+200D, U+2060, U+FEFF, soft hyphen) between two characters of a value, maps full-width forms to ASCII (a run with a full-width letter or digit, or a full-width separator such as@/.between ASCII letters or digits) and undoes defanged separators[.],(.),{.},[@],(@),{@},[:]between ASCII letters or digits; emoji joiners and lone full-width punctuation in CJK prose stay. Inrecallonly it also undoes spelled-out e-mail separators ([at],(ät),at/ät,dot/punkt,@,.; without brackets or@the local part needs.,_,-, a digit or a dot word and the domain a common TLD, sodocs at example dot comstays prose) and numbers spaced character by character (D E 8 9 3 7 …, eight or more characters, six of them digits); those findings scoreSPELLED_OUT_PENALTY_POINTS(10) below the plain form. Rules and validators run on the normalized value, the span, offsets and components are the original text, and thematched_rulesentry namesobfuscation(zero_width,full_width,defanged,spelled_out,spaced_digits). A normalized finding replaces a same-label finding it strictly contains and yields to any other, so spaced card digits that the card rule already accepts keep their plain finding. Gold convention T01 is unchanged forstrictandbalanced: spelled-out addresses stay hard negatives there, onlyrecallrows may make them positives.pii_l1closes the 8 obfuscated known gaps and gains 28 rows;l1_bilingual_rules.jsongains obfuscated probes for 8 rules. Detection change:strictandbalancednow report e-mail and IP addresses, phone, card, IBAN, Steuer-ID and SSN values behind zero-width, full-width or defanged characters;recallalso reports spelled-out e-mail addresses and spaced IBANs or passport numbers.13_l1_golden_eval464/464 positives, 366/366 hard negatives; the dev corpora (strict, exact span) are unchanged (L1 F1 50.5, no new hard-negative FP), as they contain no such values. Normalization costs about 0.2 ms per 100 KiB instrict/balancedand 0.6 ms inrecallwhen nothing is obfuscated. Validators are unchanged. - Add context-free output of checksum-strong PII values in the scored modes (K05). In
balancedandrecalla value whose validator proves a real check digit no longer needs a field name: IBAN and card numbers already reported bare, andPiiPatterngainsscored_onlyfor rules thatstrictnever runs (decision K02), reserved for unbound checksum rules. The first one,pii_steuer_id_de_context_free(base 20, checksum), reports a bare Steuer-ID of 11 digits or grouped86 095 742 719; it must pass the newvalidators::steuer_id_issued(the ISO 7064 MOD 11,10 check digit plus the BZSt digit distribution of issued IDs: one digit twice or three times, never three in a row, so12345678903or47110815008stay unreported) and stand alone, not after+or inside a longer number, range, decimal or slash path. It scores 0.55 bare, 0.80 afterSteuer-ID ist, and 0.25 behind a nearer counter-context term (Rechnungsnummer 86095742719), below both thresholds; after its field namepii_steuer_id_dekeeps the span. Golden fixture cases may namepii_mode;pii_l1.jsonlgains 24 balanced/recall rows (6 positives and 6 hard negatives per language),entity_map.yamlclassifiessteuer_id_issuedas a checksum (M3 count unchanged: same unit). Detection change: none in the defaultstrictmode (scoreboard gate on the dev corpora unchanged, 13_l1_golden_eval 442/442 positives, 352/352 hard negatives). Inbalancedandrecallbare Steuer-IDs are new findings: on the synthetic DE/EU dev set Steuer-ID recall rises from 23 to 27 of 27 gold values, with no new false positive on any dev corpus (Gretel Finance, OpenPII Nano, TAB, synthetic). Validators are unchanged. - Add structure awareness to the PII L1 detector (K04). A JSON or Python-dict key, a
key: value/key=valueline (YAML, INI, forms), an XML element or attribute (qualified by its parent element:<customer id="…"/>,<employee><id>…</id></employee>), a CSV header (;,,, tab,|) and the header row or key cell of a Markdown or HTML table now name the value they hold, like a field name right before it. The newdetectors::structureparses these fields once per text (cached on the prepared text) and normalizes keys (customerId,customer_id,Customer-ID→customer id). In every mode a key binds a field-bound rule only if the rule accepts it as its own field name (also without spaces,matrikelNummer) and the value matches the rule's value form in full; validators and the rule's extra checks (genericKennung, SWIFT, expiry coupling) decide as for a regex match, and a regex match of the same value keeps its evidence. Structural findings carrymatched_rules[].structure(json,kv,xml,csv,table) and astructure_keycomponent. For rules without a field name, a key of the K03 lexicon that names exactly the value is its context: +25 as field (side: "key"), −30 for a counter-context key such as aBestellnummercolumn, and the required context ofpii_phone_de_national_proximity;context::key_contextexposes it.pii_l1gains 28 structured rows (DE/EN, 14 positives and 14 hard negatives: order columns, generic keys the rules exclude, invalid values under accepted keys) and closes 42 known gaps;l1_bilingual_rules.jsongains structure probes for 15 rules. Detection change:strictnow reports field-bound values in JSON, key-value, XML, CSV and table carriers that it missed before. On the dev corpora (strict, exact span) L1 F1 rises from 46.2 to 50.5 with no entity losing and no new hard-negative FP (employee ID +20.2, SSN +24.2, student ID +58.7, customer ID +9.5, passport +12.5 pp); on Gretel alone, values in CSV dumps whose gold leaves them unannotated cost precision (customer ID, SSN, CVV), so neutral EN F1 moves 50.3 → 49.3 while neutral overall gains 0.2 pp. Validators are unchanged. - Add the PII context engine v2 (K03). The context of a finding is now two-sided: on the same line, before or after the value, the nearest term of a DE/EN lexicon decides. Support terms per label and counter-context terms come from
rust/src/detectors/anchor_lexicons/pii.json, which gains phone reachability phrases (erreichbar,Rückruf,call me at,can be reached at), IP, MAC and payment-card terms, and six counter-context entries for numbers that are not PII (order, invoice, shipment/tracking, ticket/case, reference/booking/transaction, serial/article/version numbers);detectors::pii::contextmaps lexicon ids to labels. Inbalancedandrecallan unbound value gains +25/+20/+15 for a support term up to 16/32/48 characters before (32 after) it, and loses 30 when a counter-context term is strictly nearer; field-bound rules keep their +25. A term does not count across a line break, a digit or;/|, and after the value not behind,/:/=/#or when it names the next value.PiiPatterngainsrequires_context: such a rule reports only with a support term and no nearer counter term, in every mode. The first one ispii_phone_de_national_proximity(base 20, format), a national number without a field name right before it (Frau Weber ist unter 0176 90817263 erreichbar.,Rückruf bitte unter (030) 1234567,0221 4711230 (Handy)). Golden rows may namepii_mode(balanced/recall), which13_l1_golden_evaluses for the scan;pii_l1.jsonlgrows from 713 to 757 rows (12 positives and 12 hard negatives for the new rule, 10 positives and 10 hard negatives inbalancedfor counter-context). The rule catalog shows the context kind and the score range per rule. Detection change:strictreports national phone numbers next to a phone term that no rule found before (new rulepii_phone_de_national_proximity); every otherstrictfinding is unchanged. Inbalancedandrecallscores now include proximity context, and a value behind a nearer counter-context term can drop below the threshold (Bestellnummer 4111 1111 1111 11110.45,Invoice no. 415-555-26710.20). Validators are unchanged. - Explain PII context scores and extend the K03 lexicon. In
balancedandrecall, everymatched_rulesentry of a PII finding now carriescontext:bonusandpenaltypoints and, when a term decided them, its lexiconanchor(fieldfor a field name bound by the rule),side(before/after) anddistancein characters;detectors::pii::context::evidencereturns the same.pii.jsongains further DE/EN terms for the existing entries (zu erreichen unter,Rückruf unter,credit card number,girocard,Kartennr,source_ip,Quell-IP,device address,bssid,Bestell-ID,invoice-id,Fallnummer,product number,Produktnummer,EAN,GTIN…), and the L1 rule catalog lists the support anchors per label and the counter-context terms.pii_l1.jsonlgains 32balancedrows (PHONE, CREDITCARD; DE and EN; 757 → 789) andl1_bilingual_rules.jsonbalanced context and counter-context probes for eight rules. Detection change: inbalancedandrecall, values next to the new terms gain or lose context points like next to the existing ones; instrict, only the context-boundpii_phone_de_national_proximityconsults the extended lexicon. Scores, spans and validators are otherwise unchanged. - Add a PII score model and the PII output modes
strict,balancedandrecall(K02). Every PII rule declares a base (1–100 points for its regex form) and the strength of its validator:checksum(+35: Luhn, ISO 7064 MOD 97-10, Steuer-ID),format(+15) ornone(+0); a value the rule binds to a field name adds 25. The score is the capped sum divided by 100. The mode is a new execution gate: RustScanGateMatrix.pii_mode(patronus_ark::PiiMode), Pythonexecution_gates={"pii_mode": "balanced"}, Ark APIgates: {pii_mode: balanced}; unknown names are rejected.strictis the default and unchanged.balanced(threshold 0.50) andrecall(0.30) report the model score inevidence_spans[].scoreandmatched_rules[].score, setconfidenceto the highest span score and drop candidates below the threshold. Every current rule scores at least 0.50, so both modes return the strict spans for now; the context engine (K03) and context-free checksum values (K05) add the candidates the thresholds gate.count_validated.py checkfails when a declared strength contradictschecksums.ark, the L1 rule catalog lists strength, base and score per rule, andrun_ark.py --pii-moderuns the opt-in modes. Detection change: none in the defaultstrictmode; goldens,13_l1_golden_evaland ARK predictions on the dev corpora are bit-identical.balancedandrecallchange scores andconfidenceonly. - Add the M3 counting rule and
scripts/l1_presidio/count_validated.py(T03). M3 now asks for 2× as many checksum-validated entities as Presidio 2.2.364 instead of 3×; a unit is one benchmark id per country whose validator computes a real check digit, format-only validation does not count.entity_map.yamlgains achecksumssection that classifies every validator of the PII registry and every Presidio recognizer withvalidate_result(43, of which 35 compute a check digit, so the target is 70).count_validated.py checkfails on unclassified validators, and with--presidioon a registry or version mismatch. Today Ark counts 3 (PAN, IBAN, Steuer-ID). No detection change. - Add the L1-PII baseline report Ark vs. Presidio (E11).
research/l1-pii/scoreboard/baseline.mdscores Ark (strict) and Presidio 2.2.364 (default,tuned) on every dev gold set per source family, corpus and entity, states M1–M4 and the hard-negative criterion, and sorts the errors into buckets with candidates for the next TRIAGE. On dev, Ark leads onneutral(exact F1 52.7 vs. 40.5 tuned) and trails onpresidio_favored(45.7 vs. 71.4, bare values without field names, grouped non-DE IBANs, IPv6 span boundaries); M1 holds for 3/9 shared ids onneutraland 4/12 onpresidio_favored, and Ark hits 2 Presidio negative cases where tuned hits none.scripts/l1_presidio/scoreboard.py report --gold DIR --engine NAME=DIR …renders the tables (P/R/F1 per family, corpus and entity, hard-negative FP per family, M1 of the first engine against each other one). Detection change: none; this is evaluation tooling and a report only. - Add Presidio-favored L1-PII gold sets and scoring per source family (E12).
scripts/l1_presidio/prepare_corpora.py --family presidio_favoreddownloads the syntheticsynth_dataset_v2.jsonof microsoft/presidio-research (MIT, pinned commit7f568b6da8a5) and the recognizer unit tests of presidio-analyzer 2.2.364 (MIT, 74 pinned test files), verifies their SHA-256 and freezes a group-atomic dev/holdout split with a SHA-256 manifest in its own directory pair (presidio_favored/). The test files are parsed withast, never executed: cases on the default recognizer with literal text and an expected count or spans become rows scored only on the recognizer's ids (scored_entities), negative cases become whole-text hard negatives typed to that id.research/l1-pii/entity_map.yamltags every gold source asark_authored(pii_l1,l1_bilingual_rules, E05 synthetic),neutral(Gretel Finance, OpenPII Nano, TAB) orpresidio_favored;entity_map.py checkfails on an untagged source. The scoreboard reports P/R/F1 per family, gates the family aggregates like the other buckets, andscoreboard.py m1checks M1 onneutralandpresidio_favoredseparately whileark_authoredis reported only (fairness rule inresearch/l1-pii/README.md). The L1 gate gets a one-offpresidio-favored-corporastep. Detection change: none; this is evaluation tooling only.
Fixed¶
-
Return the decision envelope on NTDB L2 decision-cache hits and when a deferred L3 pass is skipped (#100). A repeated scan within the cache TTL returned the cached class and confidence with
decision: Noneand withoutlabel_scoresandevidence_spans; the cache now stores them with the entry, so a hit equals the miss it replays apart from the cache diagnostics. A result whose L2 pass routed to L3 also lost its decision when L3 then did not run (the caller's conditional gate skipped it, or a request-wide early exit degraded it); the L2 arbitration is now restored as the final decision (terminality.completiondegradedafter an early exit). Detection change: none; classes and confidences are unchanged, and the affected results carry the decision a terminal L2 result carries. -
Fix the K07
iban_formsuppression for findings that start at the printed IBAN form. Inbalancedandrecall, an account field over a failing grouped IBAN (Bank account: DE00 1234 …) reported its country code and check digits asFINANCIAL_ACCOUNT_NUMBER, because containment excluded the form's first character. Detection change: none instrict; inbalancedandrecallsuch prefix findings now yield withreason: iban_form. Validators are unchanged. - End phone spans before a line break and keep NANP trunk and
00prefixes (T18, TRIAGE 2026-W43 bucket 3).pii_phone_international, the+49/0049form ofpii_phone_deandpii_phone_usno longer separate digit groups with a line break (\n,\r, U+0085, U+2028, U+2029; other spaces such as U+00A0 still separate), so+1-123-456-7890\n\n2ends before the break and digits split across lines (212\n555 0100) are no phone number.pii_phone_ustakes an optional trunk1/+1or a00international prefix in front of a 3-3-4 number into the span:1-800-123-4567,1 (800) 123-4567,1.800.432.7716,001 415 555 2671,0044 207 946 0958; with+1it now reports the whole number likepii_phone_international. The prefix is optional and needs a word boundary, so it only widens a number found before; a prefix alone (1-800-FLOWERS,Kapitel 1-800),0044 20 7946 0958and other00numbers without a 3-3-4 part stay unmatched.0049 …keeps its prefix as before.pii_l1.jsonlgains 12 positives and 10 hard negatives (DE and EN);l1_bilingual_rules.jsongains exact-span probes forpii_phone_internationalandpii_phone_deand line-break hard negatives; the newrust/tests/pii_phone_boundaries.rscovers strict, balanced and recall. Detection change: phone spans with a trunk or00prefix start earlier, spans no longer run over a line break, and digits split by a line break are no longer reported; no new match. Scoreboard gate on the dev corpora PASS: phone exact F1 +6.6 pp (overlap unchanged), L1 exact F1 +1.0 pp (DE +0.2, EN +1.4; neutral +2.1, presidio_favored +0.3, ark_authored +0.0), no new hard-negative FP;13_l1_golden_eval564/564 positives, 451/451 hard negatives. Validators are unchanged. - Stop reporting EDIFACT, X12 and SWIFT MT elements, zero-padded IDs and 6-4 digit groups as phone numbers in strict (T17, TRIAGE 2026-W43 buckets 1 + 2).
pii_phone_internationaland the+49form ofpii_phone_deno longer start right after a letter or digit (UNB+UNOC:3+520173321::2,NAD+BY+987654321), after another+(MOA+1++520173321) or after a tag with a digit and:({86:+330105789456}); a field name and:(Tel.:+49 30 1234567,tel:+31…) still opens a number, and00after+keeps an EDIFACTCOM+0049…:TEnumber.pii_phone_usneeds the area code set off by)or a separator, so811228-9874(Swedish personal number, 6-4),0000012345(ST*850*0000012345~,:62F:/0000001234/, SEC CIK) and other bare 10-digit runs are no longer matched; 3-3-4 and 3-7 forms ((617) 5550183,020-1234567) stay.pii_l1.jsonlgains 12 positives and 17 hard negatives (DE and EN);l1_bilingual_rules.jsongains DE/EN hard negatives for the three rules, and its hard-negative test now covers PII; the newrust/tests/pii_phone_precision.rscovers strict, balanced and recall. Detection change: fewer phone findings, none of them gold on the dev corpora. Scoreboard gate on the dev corpora PASS: Gretel Finance phone FP 198 → 75 (−123), phone exact F1 65.7 → 77.3 with unchanged recall; all corpora phone exact F1 +9.6 pp, L1 exact F1 +1.1 pp (DE +1.1, EN +1.1; neutral +2.0, presidio_favored +0.0, ark_authored +0.6 with 49 fewer 6-4 phone FP), no new hard-negative FP;13_l1_golden_eval552/552 positives, 441/441 hard negatives. Validators are unchanged. - Report the parenthesized area code of a US phone number as part of the span (T04, TRIAGE 2026-W42 bucket 2).
pii_phone_usopened with\b, which never holds before(after a space or at the start of a text, so(415) 555-2671was reported as415) 555-2671and missed every exact-span gold. A balanced(NXX)now starts the span in every mode; with+1in front the international rule keeps the full number as before. Unbalanced forms ((415 555-2671,415) 555-2671) keep their span.pii_l1.jsonlgates the two parenthesized known gaps and gains 10 positives and 10 hard negatives (5 each for DE and EN);l1_bilingual_rules.jsongains an exact-span probe forpii_phone_us; the newrust/tests/pii_phone_span.rscovers strict, balanced and recall. Detection change: US phone spans with a parenthesized area code start one character earlier; no finding is added or removed. Scoreboard gate on the dev corpora PASS: phone exact F1 55.2 → 65.0 (+9.8 pp, overlap unchanged), L1 exact F1 50.8 → 52.4 (EN +2.9 pp; neutral +2.8 pp, presidio_favored +0.9 pp), no new hard-negative FP;13_l1_golden_eval540/540 positives, 424/424 hard negatives. Validators are unchanged. - Reject value shapes that are no secret under credential keys and read env, INI/properties and
HTTP header lines as config fields (K05, part 1).
dlp_env_var_secret,dlp_password_assignmentanddlp_generic_credential_assignmentnow pass every value that a credential-like key binds through a value-shape filter (rules/value_shape.rs): UUIDs (also in braces or asurn:uuid:), digests after their algorithm name in the algorithm's length (sha256:<hex>,sha1-<hex>, SRIsha512-<base64>, go.sumh1:<base64>), hex digests of a digest length under a key that names a digest (hashed_password,SHA256_TOKEN,gehashtes_passwort), MongoDB ObjectIds (24 hex digits with a timestamp between 2009 and 2040), template and format fields ({client_secret},%(password)s,%s,{0},<%= … %>, an interpolation inside the value), variable references ($TOKEN,%PASSWORT%,$env:X,env:X), booleans and null words in DE and EN (true,disabled,wahr,deaktiviert), numbers with an optional unit, paths and URLs with a host but without userinfo or secret query parameter are no secret. Hex under a plain credential key stays a secret (OAuth client secrets, Flask/Rails secret keys), as does$before a word with digits. The newdlp/config_keys.rsreads env (KEY=value,export KEY=value), INI/properties (key = value,app.db.password=…) and HTTP header lines in the head of a request or response on thekey: value/key=valuegrammar of the document structure (PII K04,structure::key_value_line): a key binds the value through the rule's own key grammar and key gate, and an unquoted value with;,,,}or]inside runs to the end of the line or to an inline comment (␣#,␣;) instead of stopping at the first such character. Values with blanks, quotes or an interpolation inside, a trailing,/;, and env values with;are no config values; matches from a config line carrystructure(env,properties,http_header) inmatched_rules.dlp_l1.jsonlgains 30 rows (10 positives, 20 hard negatives, one per shape with DE and EN keys); the newrust/tests/dlp_config_keys.rschecks every carrier and shape. Detection change: fewer generic credential findings: on a 400 MB probe of public code (Rust crates, Python packages, docs) 91 of 1247 spans disappear (90 UUID-valuedChangeTokens in botocore examples, one enum constantClearTextPassword = 8107) and none is added. Unquoted config values with;,,,}or]under a credential key are now reported whole (password = {kqztrwx4711}with its closing brace). Scoreboard gate on the dev corpora PASS with unchanged numbers: exact F1 87.2, FP/MB benign code 0.1324 (5 FP), benign docs 0.1327, hard-negative FP 154, 100 KiB p95 11.27 → 11.12 ms. Golden eval: 490/490 positives, 536/536 hard negatives. Validators are unchanged. - Find HTTP auth values outside a literal header line (F-HTTP: F13 + T11).
dlp_bearer_tokenanddlp_basic_authno longer need anAuthorization:line:Bearer …andBasic …count underauthorization/proxy_authorizationkeys in env, JSON, YAML, TOML, XML, properties, shell, Dockerfile and code carriers, in logs and standalone in chats, tickets and mails. The value proves its kind instead (rules/http_auth.rs):Basiconly when its base64 is canonical and decodes to printableuser-id:password(RFC 7617) with a chosen password (no placeholder, stand-in or credential name, also read without leetspeak such asuser:p@ss, and not the user-id again);Beareronly as an issued token (RFC 6750b64token, at least 16 characters, two character classes, 3 bits of entropy per character, no sequential or repeated runs, no identifier). ABearervalue that parses as a JWT is left todlp_jwt_token, a provider token to its provider rule.dlp_session_cookiereports the session cookies of servers and frameworks (JSESSIONID,PHPSESSID,ASP.NET_SessionId,connect.sid,laravel_session,__Host-/__Secure-names) also without aCookie:line, asname=valueor a quoted key and value; generic names (session,sessionid,sid) still need aCookie:/Set-Cookie:line, and every value must look like an issued session ID.dlp_basic_authnow has validator strengthstructureanddlp_bearer_tokenformat, both with an RFC as format source (rule_sources.yaml); the RFC 7617 example credentials are a documented example value (K06). New generatorsexpress_session_idandlaravel_session; random base64 (http_basic_credentials) is a lookalike thatdlp_basic_authmust not report.dlp_l1.jsonlgains 42 rows (20 positives, 22 hard negatives; DE and EN) including the swagger-ui test values (Bearer abc123.xyz.789,Bearer secret_token,Basic application:secret); the newrust/tests/dlp_http_auth.rschecks every carrier and the valid and invalid Basic decodings. Detection change: on the dev split the exact F1 ofcredential.http_basic_auth,credential.http_bearerandcredential.session_cookierises from 12.2/14.0/14.0 % to 100 % each (recall 7.5 % → 100 %), six hard-negative FP of Basic example values withoutuser:passwordand the three benign-code FP of the old header rules (Bearer abc123.xyz.789,Bearer secret_token,Proxy-Authorization: Basicofuser:p@ss) disappear. Scoreboard gate on the dev corpora PASS: exact F1 83.2 → 87.2 (CREDENTIAL 81.9 → 96.8, SECRET_TOKEN 93.6 → 99.2), FP/MB benign code 0.2119 → 0.1324 (8 → 5 FP), benign docs unchanged, hard-negative FP 160 → 154 with no new FP row, 100 KiB p95 10.56 → 10.54 ms. Golden eval: 480/480 positives, 516/516 hard negatives. - Leave a
BearerJWT todlp_jwt_tokenonly when that rule can match it (F-HTTP review). A structurally valid JWT with empty claims (e30) or a signature shorter than ten characters parses but falls outside thedlp_jwt_tokenpattern, sodlp_bearer_tokenrejected it and nothing reported it. The bearer gate now defers only JWTs in the shape of the JWT rule (header and claimseyplus at least ten characters, a signature of at least ten) and judges every other value as an opaque token. Detection change: suchBearerJWTs are now reported bydlp_bearer_tokenwhen they look like an issued token; JWTs the JWT rule matches are unchanged. - Find credential values under compound, camelCase, quoted and typed keys (T09).
dlp_password_assignment,dlp_generic_credential_assignment,dlp_csrf_tokenanddlp_password_hashnow start at the credential noun without\b, so compound keys (db_password,service_secret,LAGER_DB_PASSWORT) and camelCase keys (dbPassword,serviceSecret,ClientSecret,csrfToken,PasswordHash) match; the new key gate (RuleMeta::key_gate,likeness::key_shape) reads the prefix before the noun and rejects a noun that merely ends a word (mypassword,DBPASSWORD,Benutzerkennwort). Between key and value the rules accept the closing quote of a quoted key ("client_secret":,'password' =>, escaped\"db_password\":inside a JSON string), a subscript (os.environ["DB_PASSWORD"] =), a string type (const DB_PASSWORD: &str =,dbPassword: string =, GodbPassword string =),=>and the XMLvalueattribute after akey/nameattribute (<add key="DbPassword" value="…" />); the assignment rules take an escaped value in a notebook cell or JSON string (\"…\"). Compound and quoted keys name fields, so their values must suit one: no identifier in a naming convention ("github-copilot","prefixItems"), word, credential name or stand-in, no version range ("^1.0.0"), no code (also in quotes), a passphrase with a capital, digit or symbol, and after a quoted key or=>no unquoted value (case"apiKey":u=…,'password' => !empty(…)). Compound…tokenkeys of cursors, handles and counts (nextPageToken,continuation_token,pathToken,max_token,csrf_tokenfor the generic rule) and…secretkeys of Kubernetes/Helm objects (existingSecret,imagePullSecret,tls_secret) are no credentials; a bare value cut at the 150-character capture limit is none either (id_token=with a long example JWT). Code at the start of a bare value (casts and parameter lists, arrow functions, comparisons, negated calls) is no value.dlp_csrf_tokengains the value gatelikeness::is_token_value(no word or name such as"X-CSRF-Token", no member access);dlp_password_hashno longer takes the backslash of an escaped closing quote into a bcrypt or Argon2 value. A generic key with a compound prefix in a URL userinfo (https://github_token:x-oauth-basic@…) belongs to the URL rules (K08). Word boundaries of the keys are ASCII ((?-u:\b)), which makes the generic rule's regex about 25 times faster on benign code;dlp_aws_secret_keyalready matched every AWS key shape and is unchanged.dlp_l1.jsonlgains 31 rows (17 positives, 14 hard negatives; JSON, YAML, TOML, XML, TS, Rust, PHP, HTTP and notebook carriers, DE and EN) and the JSONpassword_hashgolden is no longer a known gap;l1_bilingual_rules.jsongains hard negatives for the three rules; the newrust/tests/dlp_key_shapes.rschecks every key shape per carrier and the silent code, field-name, cursor and reference shapes. Detection change: on the dev split the exact span F1 on the synthetic carriers rises from 81.6 % to 84.4 % (TP 1225 → 1294, precision 98.2 % → 98.8 %, no positive lost); every structured-carrier miss of the five rules is found (69 values; the remaining misses are prose, F-HTTP and opt-in classes), hard-negative FP drop from 22 to 16 (six example JWTs underid-tokenkeys) with no new FP row; benign-code and benign-doc spans are unchanged. Scoreboard gate on the dev corpora PASS: exact F1 80.6 → 83.2 (CREDENTIAL 69.6 → 81.9, PASSWORD_HASH 80.6 → 91.9, SECRET_TOKEN 89.7 → 93.6), FP/MB benign code unchanged (8 FP), hard-negative FP 166 → 160, 100 KiB p95 9.55 → 9.43 ms. Golden eval: 460/460 positives, 494/494 hard negatives. - Capture generic credential values exactly (F-GEN2: T04 + T08). A quoted value of
dlp_password_assignmentanddlp_generic_credential_assignmentnow runs to its closing quote, with spaces,;,,and}inside (passwort = "blau Drache 47 Zaun",password='ab;Kx7…'); a bare value or an unclosed quote still stops at them.dlp_env_var_secretreports the value without its quotes and without a trailing;/,(export DB_PASSWORD='…',ENV KEY="…",KEY = "…";in Python, Java and Ruby), no longer takes the next line after an emptyKEY=, and also matches YAML/compose env maps (KEY: "value",KEY: 'value', a bare value up to the end of the line). Rules may now hold their value in one capture group per quoting style; the engine reports the first group that took part in the match (NativeRegexDetector::value_group). The new value gatelikeness::is_env_valuedrops code constants (DATA_API_KEY = '.data-api',AUTH_FLOW_PASSWORD = "password"), credential names, calls and index access (config.get(…),os.environ[…]), format placeholders and paths. A quoted value with spaces must read as a passphrase, not as a message ("Enter your password","Bitte Passwort eingeben","too short", a closing./?/:, more than six words), a placeholder ("change me") or a template ({{ … }},${…},$(…),%s). A quote that closes a string opens no value ("Password:"),d?i.createElement(in minified code). The T06 example check also counts a marker cut to the field length at the end of a word (Eexampl0123!,Ttestke1234!; at least six letters ofexample(key),testkey,sample,beispiel,muster), and a generic value of at least 32 hex digits is machine-issued, so repeated chunks (deadbeef…,1234567890…) reject it as for a fixed format; shorter hand-typed repeats (deadbeefdeadbeef,abcabcabcabc) stay weak but real passwords.CredentialTemplateHookno longer reads base64 padding askey=value, soDB_PASSWORD: <base64>=in a Kubernetes Secret keeps its span.dlp_synth/rules.jsongainsquotedgenerators (quoted_passphrase,quoted_punctuated_password) that every probe wraps in quotes;dlp_l1.jsonlgains 33 rows (TOML, YAML, INI, compose, CI, shell, Dockerfile, Java, Python, JS and DE/EN prose positives; code constants, label quotes, messages, templates and references and hex fill values as hard negatives) and gates a Kubernetes Secret known gap;l1_bilingual_rules.jsongains hard negatives for the three rules; the newrust/tests/dlp_value_capture.rscovers the spans per carrier. Detection change: on the dev split the 15dlp_env_var_secretboundary errors (values with quotes or a trailing;) are gone, 6 CI-YAML values are found, exact span F1 on the synthetic carriers rises from 77.4 % to 81.6 % (TP 1204 → 1225, precision 89.0 % → 98.2 %, no positive lost), hard-negative FP drop from 134 to 22 with no new FP row, and benign-code spans drop from 38 to 8 (all 30dlp_env_var_secretFP, i.e.DATA_API_KEY = '.data-api'andAUTH_FLOW_PASSWORD = \"password\"). Validators are unchanged. - Keep escaped quoted env values and stop bare shell values at command separators (F-GEN2 review).
dlp_env_var_secretalso takes a value between escaped quotes (KEY=\"…\",KEY=\'…\',KEY: \"…\") as found in serialized source, JSON strings and logs; the broad value pattern before F-GEN2 caught these, the quoted branches did not.likeness::is_env_valuetreats an escaped opening quote as opening the value, never as closing the surrounding string. A;followed by a path (KEY=value;./bin/report,;/usr/bin/x,;~/x) now ends a bare value as a shell command separator; any other;stays part of a.envvalue (Kx7;pQ2vL9).rust/tests/dlp_value_capture.rsgains regression tests for both. Detection change: escaped quoted env assignments are reported again (value without the escaped quotes); bare env values no longer run over;into a following path command, so those spans end at the separator. - Generic credential values need password likeness (F-GEN1: F11 + T03 + T10).
dlp_password_assignmentanddlp_generic_credential_assignmentno longer report code expressions after a credential key (calls such astoken = get_token(), index and member access such asos.environ[…],this._input.value,$authTokens[$url], Rust pathsToken::Literal(c), type annotations such astoken: Option<&str>,password: str),string|null,Arc<…>, format placeholders%s, ABNF repetitions), keys and labels ending in:(password:\n type: string), file paths, a shell variable as key (docker -v "$PWD:/work"), DE/EN dictionary words and word forms in prose („Passwort ist falsch“, „Token ist abgelaufen“, "the password is stored"), credential names in one naming convention (GIT_PASSWORD,passwordGenerator,clientSecret) or test stand-ins (password: "password","pass","topSecret",foo001). A value with a digit or another symbol is never a word, so weak but real passwords (hunter2,Sommer2026!,P@ssw0rd,letmein,abcabcabcabc) stay findings.dlp_url_userinfo_passworddrops stand-in and credential-name passwords of test suites (user:pass,user:secret,token:MY_GITHUB_TOKEN,gitlab-ci-token:realtoken) and the pseudo-passwordx-oauth-basic. The new ruledlp_url_userinfo_token(SECRET_TOKEN, subtypeaccess_token, format source GitHub) reports the token in the username ofhttps://<token>:x-oauth-basic@…if it has two character classes, 3 bits of entropy per character and no sequential or repeated runs. The checks live in the newrust/src/detectors/dlp/rules/likeness.rsand run through the newRuleMeta::value_gate.dlp_l1.jsonlgains 33 rows (code, prose, INI, CLI, log and JSON positives with weak passwords and token URLs; Rust/Python/TS/PHP/shell expressions, DE/EN prose and test-suite URLs as hard negatives);l1_bilingual_rules.jsongains hard negatives for the three rules and a case for the new one; the newrust/tests/dlp_generic_precision.rscovers Rust, Python, TS/JS, Go, PHP and shell expressions, prose, stand-ins, test-suite URLs, weak passwords and token URLs. Detection change: on the dev split the benign-code findings of the three rules drop from 192 to 3 (−98 %; the rest are percent-encoded test passwords), benign docs lose both$PWD:findings, and thestrictoutput on the synthetic carriers is unchanged. - Tighten the F-GEN1 likeness checks (review of PR #129). A DE/EN suffix or participle shape
alone no longer makes a value a word form: the stem must be a dictionary word (also with a
dropped
e, a doubled final consonant ory→i) or read like a word (at least a quarter vowels, no run of more than three consonants), so random letters ending ining,edoricafter a credential key stay findings.dlp_url_userinfo_tokenno longer reports a word or name with a numeric suffix only (a handle such asjane.doe2026) as a token. Detection change:dlp_password_assignmentanddlp_generic_credential_assignmentregain unquoted random alphabetic secrets with suffix-like endings (fewer false negatives);dlp_url_userinfo_tokendrops numeric-suffixed usernames (fewer false positives). Dictionary words, word forms with a known or pronounceable stem and all existing positives are unchanged. - AWS access key IDs need a credential prefix and exactly 16 Base32 characters, and AWS secret
access keys are found by their structural context (F05).
dlp_aws_access_keytakesAKIA,ASIA,ABIA,ACCAor legacyA3T…and exactly 16 Base32 characters (before: alsoAGPA,AIDA,AROA,AIPA,ANPA,ANVAand 16 or more upper-case alphanumerics); group, user, role and policy unique IDs are no credentials.dlp_aws_secret_keytakes 40 base64 characters with upper and lower case letters as a whole run (before: the first 40 characters of any longer run, also with=), and finds them after AWS key names and labels in every naming style (aws-secret-access-key,AwsSecretAccessKey,aws.secretKey,AWS_SECRET_KEY, HTTP header, XMLkey="…" value="…", typed constants, escaped JSON), in DE/EN prose up to two lines after the label, right after an access key ID (SDK calls, IAM credentials CSV,mc alias set), and after a genericsecret_key/secret-key/secretKeywith AWS context (Terraformprovider "aws"/backend "s3",.s3cfg, Springcloud.aws, an access key ID oraccess_keykey nearby); a Django/FlaskSECRET_KEYis no anchor. The newrust/src/detectors/dlp/aws_context.rsholds the value check and the context gate.dlp_l1.jsonlgains 24 rows (Terraform,.s3cfg, Spring YAML, CLI,aws configure, XML, log, chat and mail positives; IAM user IDs, key IDs outside Base32 or of another length, 41-character and hex values, references, example/test fills and documented formats as hard negatives), with new generators (ABIA,ACCA) and lookalike generators indlp_synth. Detection change: access key IDs with other prefixes, a0/1/8/9or another length and secret keys that are hex, single-case or part of a longer run are no findings any more; AWS secret access keys in the new contexts are new findings. On the dev split thestrictoutput on benign code and benign docs is unchanged; synthetic carriers gain 16 gold secret keys (cloud.aws.secret_access_keyrecall 24/40 → 40/40) and no other span. - npm tokens need a valid CRC32, and every documented GitLab token prefix is found (F04).
dlp_npm_tokentakesnpm_and exactly 36 base62 characters whose last six are the CRC32 of the first 30 (before: 36 or more, no checksum) and scores atchecksumstrength, so a bare token scores 0.85 instead of 0.65. A generic assignment, URL or header rule no longer reports a value that starts with a wholenpm_token shape whose CRC32 fails.dlp_gitlab_pattakesglpat-and exactly 20 characters (before: 20 or more) or the routable form (payload,., a two-character version and a seven-character base36 check value, reported whole; before cut at the dot). The new ruledlp_gitlab_token(SECRET_TOKEN,access_token) finds deploy (gldt-), runner (glrt-, also routable), runner registration (GR1348941), pipeline trigger (glptt-), feed (glft-), incoming mail (glimt-), Kubernetes agent (glagent-), CI/CD job (glcbt-), SCIM (glsoat-) and feature flag client tokens (glffct-) with the lengths of the gitleaks rules; the new ruledlp_gitlab_oauth_secret(API_KEY,secret_key) finds OAuth application secrets (gloas-). Both are on by default; the entity map moves the GitLab tokens frombaseline_onlytoshared(vcs.gitlab.token,vcs.gitlab.oauth_app_secret).dlp_l1.jsonlgains 32 rows (tokens of every prefix in.env, YAML, JSON, TOML, INI, code, CLI, HTTP, log and prose; wrong check values, lengths and alphabets, example/test fills, references and documented prefixes as hard negatives), with new generators and lookalike generators indlp_synth. Detection change:npm_values with a wrong CRC32 or another body length andglpat-values of another length are no findings any more; GitLab deploy, runner, trigger, feed, mail, agent, job, SCIM, feature flag and OAuth tokens are new findings. On the dev split thestrictoutput on synthetic carriers, benign code and benign docs is unchanged. - Find GitLab runner tokens created through a registration token (
glrtr-), workspace tokens (glwt-) and the current routable token form (F04 review).dlp_gitlab_tokenalso takesglrtr-with 20 characters and the current routable form of GitLab's HTTP router andRoutableTokengenerator forglrt-,glrtr-andglwt-: a base64url payload of 27–300 characters,.01., the payload length as two base36 digits and a seven-digit base36 CRC32 (not verified);dlp_gitlab_pattakes this form forglpat-too. Workspace tokens are only taken in the routable form. Another version, an upper-case or wrongly sized check value, a payload of fewer than 27 characters and undocumented neighbour prefixes (glwtr-,glrtrr-) stay no GitLab token (rust/tests/dlp_npm_gitlab_tokens.rs). Detection change:glrtr-andglwt-tokens and current routable runner and personal access tokens are new findings, reported whole; no finding is removed. - GitHub tokens need their exact length and, for classic tokens, a valid CRC32 (F03).
dlp_github_tokentakesghp_/gho_/ghu_/ghs_/ghr_and exactly 36 base62 characters whose last six are the CRC32 of the first 30 (before: 32 or more word characters, no checksum);dlp_github_pattakesgithub_pat_and exactly 82 characters (before: 36 or more).validator_strengthofdlp_github_tokenischecksum(wasformat), so a bare token scores 0.90 instead of 0.70 and stays inbalancednext to a counter term. A generic assignment, URL or header rule no longer reports a value that starts with a wholegh…_token shape whose CRC32 fails.dlp_l1.jsonlgains 24 rows (tokens of every prefix in.env, YAML, JSON, TOML, code, CLI, log, chat and mail; broken check values, wrong lengths, example/test fills, references and the documented format as hard negatives), andsecret-token-pos-01uses a checksum-valid token. The synthetic generatorweak_repeated_chunk_github_tokennow carries a CRC32 over its whole body. Detection change:gh…_values with a wrong CRC32 or another body length andgithub_pat_values of another length are no findings any more, also not as generic credentials. On the dev split thestrictcarrier output is unchanged and the benign-code corpus loses 13 false positives (GitHub token test fixtures), with no new span. - Skip GitHub token shapes of a wrong length in generic rules too (F03 review). A generic
assignment, URL or header rule (
dlp_env_var_secret…) no longer reports a value that is aghp_/gho_/ghu_/ghs_/ghr_orgithub_pat_prefix with a whole-token body of another length than 36 or 82 characters (GITHUB_TOKEN=ghp_and 35 characters), matching the provider rules, which reject those lengths;RuleMeta::body_lenholds the exact body length and the shortest token-shaped body (32 and 36, the former minimums). Shorter bodies and bodies that end in other punctuation stay generic credentials. Detection change: fewer genericCREDENTIALspans for wrong-length GitHub token shapes; exact-length tokens are unchanged. dlp_btc_wifaccepts testnet WIF keys and scores at checksum strength (F02). Base58Check now takes the version byte of mainnet (0x80;5…,K…/L…) or of testnet, signet and regtest (0xef;9…uncompressed,c…compressed), still with a secp256k1 scalar in range and the0x01flag for compressed keys; other version bytes, flags and scalars are no key. Testnet keys report severitytest.validator_strengthischecksum(wasformat), so the model score of a WIF key without context is 0.65 instead of 0.45.dlp_l1.jsonlgains 22 rows (testnet keys in faucet.env,bitcoin-cli -testnet/electrum --testnet, YAML, TOML, JSON, wallet code, log, chat and mail; testnet-shaped example/test fills, broken check values, references and the documented format as hard negatives; a checksum term as balanced hard negative). Detection change: checksum-valid testnet WIF keys are reported asCRYPTO_KEY(before: not matched), and inbalanceda WIF key without a credential term is kept (before: dropped below the threshold); a nearer checksum, hash or ID term still drops it there. On the dev split thestrictoutput is unchanged;balancedgains 39 gold WIF carrier spans and no other span.dlp_eth_private_keyreports0xand 64 hex digits only in a key context (F01). The value needs a key term in its context, i.e. its line and up to five lines above it back to the previous hex value (private key, privkey, secret key, signing key, mnemonic, keystore, privater/geheimer Schlüssel; on the value's line or as its label also wallet, signer, accounts, pk, key, Schlüssel). A term that labels the value (only separators in between, also across line breaks) decides, so a hash, root, ID or chain-data label (tx hash,transactionHash,blockHash, Transaktionshash, checksum, Prüfsumme, Merkle/state root, Zustandswurzel, topic, salt, nonce, public key …) rejects the value. Without a label, a key term proper anywhere in the context supports the value, so a hash or ID word elsewhere in the sentence does not veto it; otherwise the nearest term decides. A term after the value counts within 40 characters on its line unless it names the next field. The value must also be a secp256k1 scalar (1 ≤ k < n). The gate is a newRuleMeta::context_gateand applies in every DLP mode.dlp_l1.jsonlgains 20 rows (CLI flags,DEPLOYER_PK, wallet constructors, chat/mail context; tx/block hashes, event topics, CREATE2 salt, state and Merkle roots as strict hard negatives). Detection change: a bare 0x-64-hex value and one next to a hash, root or ID term is no longer reported, also instrict(before: every 0x-64-hex value). On the dev split the output is unchanged (all 40 Ethereum carrier positives stay findings; benign code and documents had no such match).- DLP prefix rules report a value only as a whole token (T01). The 20 provider rules with a
fixed prefix (
sk-ant-,hf_,AKIA,ghp_,npm_,xoxb-…),dlp_eth_private_key,dlp_btc_wifanddlp_jwt_tokendeclare a token boundary (RuleMeta::boundary): the characters right before and after the value may be no[A-Za-z0-9_]and none of the provider alphabet (-for base64url-style tokens), and a value with a+//neighbour inside a base64 run of at least 16 characters with upper case, lower case and digits is part of a blob. Escapes that end in a letter or digit still delimit a token (\n,\t,\uHHHH,\xHH,%HH).dlp_openai_legacydrops its\banchors for the same check, so it also finds keys after%3Dor\n. Key-anchored rules (dlp_aws_secret_key, assignments) are unchanged; checksum validation stays with F02–F05.dlp_l1.jsonlgains 28 rows (lockfile checksums, Docker digests, go.sum hashes, identifiers; escape-delimited positives). Detection change: a prefix token glued into an identifier (load_hf_…,resolve_npm_…,CachedAKIA…Policy), a hex run (Cargo.lock checksum, sha256 digest, ABI calldata) or a base64 blob (go.sum, npm integrity, data URI) is no longer reported. On the dev split this removes one benign-code FP (a JWT behindghs_…_in a test fixture); F1 and hard-negative FP are unchanged. - Keep each rule's own score in
matched_rulesfor PII findings behind an undone obfuscation (K06). A full-width bare German IBAN reportedpii_iban_genericwith the 0.90 ofpii_iban_deinstead of its own 0.80; the recall-only penalty now applies to each rule's own score. Detection change:matched_rules[].scoreonly, evidence spans and verdicts are unchanged. - Annotate every real entity in the PII L1 golden positives and check gold values against the validators (T02). Positives in
pii_l1.jsonlnow also carry the card number next to a CVV (15 rows) or a card expiry (4 rows), the phone number inpii_l1-email-pos-01and the IBAN inpii_l1-swift-code-pos-06;generate_l1_goldens.pytakes them asalso=(label, value).rust/tests/pii_gold_hygiene.rsrequires that every gold value passes a validator of its label (known gaps only forCREDITCARD,IBAN,STEUERID) and that every other L1 finding on a positive is gold or a listed detector FP. The seven Luhn-invalidcredit_cardvalues indynamic_pii.jsonlare marked"checksum_valid": falseand no longer count as L1 gold in12_l1_pii_fixture_eval(now 69 TP, 0 FP, 2 FN); the L3 NER still sees them. Validators are unchanged. Detection change: none; this is evaluation tooling and gold data only. - Score
rust/examples/12_l1_pii_fixture_eval.rson character offsets. It compared the UTF-8 byte offsets ofnative:piispans with the code-point offsets ofdynamic_pii.jsonl, so every span after a non-ASCII character counted as one FP and one FN. Gold spans outside the text now abort the run, and the example's unit tests run withcargo test. Detection change: none; this is evaluation tooling only. - DLP secret rules no longer report documented example and test values (T06). A shared check next
to
is_secret_valuerejects values with a marker word:EXAMPLE,SAMPLE,BEISPIELorMUSTERat the start or end of a word (not inside one, e.g.resampler),TEST/TESTKEY,FAKEorDUMMYas a whole word, or any marker repeated, so a marker inside random base62 does not count and the Stripe modesk_test_is no marker. Values mostly made of repeated chunks or same-character runs are rejected for fixed-format provider, key, hash, JWT, signed-URL and session-ID rules, also when a generic rule (dlp_env_var_secret, assignments, URLs) captures the provider-shaped value. Values that count as low-variety only through a single sequential run (AKIA1234567890ABCDEF, hand-typed demo keys) and low-variety generic password/credential assignments are rejected only with explicit example context (example, sample, dummy, fixture, Beispiel, Muster … in the line or the three lines above; host names such asexample.comdo not count). PEM rules check the key body. Weak but real credentials (password=abcabcabcabc, a token with a1234567890abcdefrun, a provider token with one repeated chunk) stay findings; newweakgenerators indlp_synth/rules.jsonpin them, and per-provider DE/EN hard negatives were added todlp_l1.jsonl. Their provider-format values are generator recipes with a newfillword (patronus_ark.dlp_synth.recipes,patronus_ark::golden_recipes), and the provider values ofl1_bilingual_rules.jsonand the tests are assembled at test time, so no commit adds a token in an exact provider format. Detection change: on the dev carrier split, hard-negative FP ondoc_example/test_keyrows fall from 3 590 to 220 (−93.9 %) with unchanged positives; documented vendor examples such as the AWS documentation access key ID (AKIA…EXAMPLE) are no longer findings. Test fixtures that used fill values (aB12aB12…,abcDEfGh…) now use random values. - Value-capturing DLP credential rules (
dlp_env_var_secret, password and generic credential assignments,dlp_credential_in_url,dlp_signed_url_signature,dlp_url_userinfo_password,dlp_csrf_token,dlp_bearer_token, Basic auth and session cookies) no longer report placeholders or references (T07). Placeholders are<…>/<…>, masks (********,xxxx-xxxx),TODO,CHANGEME,REPLACE_ME,BITTE_AENDERNand all-upper or all-lower word sequences with a placeholder word (YOUR_…_HERE,HIER_…_EINTRAGEN,YOUR-API-TOKEN,dein-api-token,INSERT_…). References are$VAR(a$word with digits and without_, such as a password, stays a finding),${…},$(…),${{ … }},%VAR%,{{var}},{%…,#{…}, Terraformvar./local./data./module., and environment accessors (os.environ[…],os.getenv/os.Getenv,System.getenv,Environment.GetEnvironmentVariable,ENV.fetch/ENV[…],process.env,import.meta.env,env::var,env!,getenv,env(…)). The check reads the text after the value, so captures cut at(,[or}are recognised, and it stops at quotes and escape backslashes (JSON-escaped notebooks). Real values next to references and placeholder-like real passwords (SCREAMING_SNAKE without a placeholder word, mixed case such asYourDog2026!) stay findings; two newweakgenerators pin them, and DE/EN hard negatives were added todlp_l1.jsonlandl1_bilingual_rules.json. General code expressions as values stay with T03. Detection change: on the dev carrier split, hard-negative FP onplaceholder/env_refrows fall from 277 to 0 with unchanged positives (exact TP 1 183); benign-code dev FP fall from 372 to 367 (your-client-secret-if-required,YOUR-API-TOKEN), benign docs unchanged. - DLP allowlist for documented values, test keys and further references (K06). Every secret rule
drops vendor-documented example values listed in the new
research/dlp/documented_values.yaml(AWS documentation keys, the Bitcoin wiki WIF example, the RFC 8410 Ed25519 key), which holds them only as SHA-256 of the normalized value with their source, and every JWT whose claims are the documented example claims of jwt.io (sub1234567890,nameJohn Doe) or RFC 7519/7515 (issjoe,exp1300819380), under any header and signature. Publicly known test keys (Hardhat Network/Foundry Anvil default accounts #0–#2) stay findings withseverity: test. The value-capturing credential rules of T07 also drop Python%(name)sand{name}/{0}/{name!r}, Ruby/Puppet%{name}, Maven@name@, replace tokens__NAME__, cmd!NAME!, Concourse((name)), the YAML tags!secret,!vault,!env_varand CloudFormation!Ref/!Sub/…, secret-manager references (1Passwordop://, valsref+…://, Azure@Microsoft.KeyVault(…), AWS Secrets Manager/SSM ARNs, GCPprojects/…/secrets/…,/run/secrets/…, also when the rule captures a part of the ARN),null/None/nil/undefined, the JKS defaultchangeit, redaction words (REDACTED,***REMOVED***,***ENTFERNT***,GESCHWAERZT,**None**), the DE last words…_EINGEBEN,…_AUSFUELLEN,…_EINSETZEN,…_HINTERLEGEN, and values that only name the credential (my-access-token,secret_token,SECRET_PASSWORD,MEIN_PASSWORT,auth.password). Delimited names that look like the rest of a password ({kqztrwx4711},__Kx7pQ2vL9rT4__) and words outside the credential-name vocabulary (actual-secret) stay findings. Inline suppression comments (gitleaks:allow,pragma: allowlist secret) are deliberately not honoured: in a DLP gateway they would let any sender exempt a secret. Detection change: on the dev split, benign-code FP fall from 374 to 350 and benign-docs FP from 4 to 3 (None/nulldefaults,my-access-token,auth.password,**None**,Bearer secret_token, a jwt.io RS256 documentation token); carrier spans are bit-identical (1 541), no span is new anywhere. 30 newdlp_l1goldens (DE/EN, 18 hard negatives, 12 positives) and eight hard negatives inl1_bilingual_rules.json. scripts/l1_dlp/run_baselines.py run --tool detect-secretsno longer aborts the whole run when a plugin raises on one file (a YAML date the detect-secrets YAML transformer cannot parse in the benign-code dev split). That file is listed undererrorsin<output>.meta.json(rowid, file name, exception class only) with no spans, and every other file keeps its findings, as inlatency.py. The metadata of all three tools now carrieserrors, and the summary line counts them. Detection change: none; this is evaluation tooling only.
[0.1.10] - 2026-10-16¶
Added¶
- Add the audited, high-precision Injection L1 governance rule catalog
(
rust/src/detectors/injection/rules/governance_attack_regressions_0_1_9.json, 18 rules such ascross_tool_next_request,nested_system_overrideandgerman_policy_replacement). It covers tool-output next-request overrides, nested system delimiters, German and English policy replacement, unrestricted modes, raw system-role boundaries, privileged tool calls, administrator grants and embedded new-task instructions. The end-to-end regression testrust/tests/governance_attack_regressions.rspins every attack as terminal with its audited rule id and keeps nearby quoted, documentary and negated forms nonterminal. Detection change: Injection L1 now reaches a terminalinjectiondecision for these governance attacks; the calibrated Injection L1 threshold is unchanged and the existing goldens and hard negatives are unaffected.
[0.1.9] - 2026-10-16¶
Added¶
- Add the
shell_commandcategory, backed by the revision-pinnedpatronus-studio/cat-shell-risk-commandermodel (Apache-2.0). It scores shell commands and scripts (POSIX, PowerShell, CMD) natively in the L1 phase and returnsshell_risk/safewith the decision score, threshold policy and evidence spans. The model is downloaded and SHA-256-verified like the other assets, baked into the Docker image, and the category is accepted by the library,ark-api, the Cube worker template andark-coordinator. Asset manifest change: newshell_command/cat_shell_risk_commanderbundle. Detection change: new category only; existing categories are unchanged. - Validate live IONOS scale-down from 12 to 8 Cubes and an end-to-end 8-to-9-to-8 Cube lifecycle, including worker setup, Coordinator/ALB routing, GPU inference, async scans, drain, and API removal.
- Add an external Triton watchdog that validates real GPU inference, with a hardened systemd timer and deduplicated TLS SMTP failure/recovery notifications. Credentials and deployment addresses remain in private host configuration.
- Add a reproducible Microsoft Presidio baseline runner for the L1-PII evaluation (
scripts/l1_presidio/run_presidio.py) with a pinned environment anddefault/tuned(EN + DE) profiles. It writes predictions in theexternal_pii_evalformat. Detection change: none; this is evaluation tooling only. - Add an Ark PII-L1 prediction runner (
scripts/l1_presidio/run_ark.py) that scans normalized Gold JSONL through the Python bindings and writes the same prediction rows as the Presidio runner, including per-spanscoreand the original Ark label, with a timestamp-free metadata sidecar pinned to the native build. Detection change: none; this is evaluation tooling only. - Add the L1-PII scoreboard (
scripts/l1_presidio/scoreboard.py).gate --head WT --base-sha SHA --dev DIR --holdout DIR --cache DIRscores the Ark build of a checkout and of its base commit with exact and overlap P/R/F1 per entity, for all L1 entities and for DE/EN, counts predictions on hard-negative spans and exits 1 on an F1 loss above 0.5 pp or a new hard-negative hit. Base builds run offline fromgit archiveinto the cache; predictions are cached per source key, results per commit.scorescores a single prediction file (Ark or Presidio). Detection change: none; this is evaluation tooling only. - Add the L1-PII latency benchmark (
scripts/l1_presidio/latency.py).runmeasures Ark through the Python bindings of a release build of the checkout (ark), the native Rust gateway (ark-native,rust/dev/l1_latency_workloads.rs) and Presidiodefault/tunedon fixed DE/EN workloads of one sentence, exactly 4 KiB and exactly 100 KiB, each engine in its own process, and reports p50/p95/p99 per size, the M4 check (100 KiB p95 ≤ 10 ms) and the Presidio/Ark ratio. Debug builds are refused.compareexits 1 when the Ark 100 KiB p95 grows by more than 10 % between two reports of the same machine. First VPS results are inresearch/l1-pii/scoreboard/latency.md. Detection change: none; this is evaluation tooling only. - Run the L1 golden evaluation (
rust/examples/13_l1_golden_eval.rs) in the CIrustjob for both suites (pii_l1.jsonlanddlp_l1.jsonl, with--locked), so a positive span miss or a hard-negative hit fails the pull request, not only the L1 gate on the VPS.scripts/tests/test_ci_golden_eval.pykeeps the step in place. Detection change: none; this is CI and evaluation tooling only. - Add the L1-PII entity map (
research/l1-pii/entity_map.yaml) that assigns every Ark PII label, every Presidio 2.2.364 entity and every external corpus label to one benchmark id with a scope (shared,ark_only,presidio_only,semantic), plusscripts/l1_presidio/entity_map.py checkthat fails when a registry label is unmapped or the map disagrees with the corpus manifest or the runner label maps. Detection change: none; this is evaluation tooling only. - Add
scripts/l1_presidio/prepare_corpora.py, which downloads the pinned Gretel Finance, OpenPII Nano and TAB sources, verifies their SHA-256, and freezes a group-atomic dev/holdout split (fixed seed, 60 % holdout) with a SHA-256 manifest and a.readymarker per directory; reruns must reproduce a frozen split byte for byte. Gretel Parquet is read by a dependency-free reader so the script runs in the Presidio venv. Detection change: none; this is evaluation tooling only. - Add
scripts/l1_presidio/synth_goldset.py, a synthetic DE/EU L1-PII goldset generator. It covers seven document types (letter, form, invoice, medical letter, JSON, CSV, support ticket) in German and English. Every benchmark id ofresearch/l1-pii/entity_map.yamlappears in both languages, with checksum-valid values. Spans are code-point offsets, and hard-negative spans are tagged. Dev uses a public seed; the holdout uses a secret seed file. Both outputs are frozen with a SHA-256 manifest and.ready. The L1 gate creates them once. Detection change: none; this is evaluation tooling only. - Define the L1-PII gold convention for reserved placeholder values (
research/l1-pii/README.md): RFC 2606/6761 domains (example.com,.invalid,.test,.example,.localhost) and NANP555-01xx/+1 000-000-0000numbers are PII and gold positives; hard negatives exclude values by form only.demo_pii_dlp_blind_en_tech.jsonlrowcu-07now expectsEMAILandPHONE, and thepii_l1hard negative@localhostbecomes a non-reserved single-label host. Tests enforce the convention acrosspii_l1,demo_pii_dlp*and the E05 goldset. Detection change: none; Ark already reports these values. - Add journaled IONOS fleet reduction with target planning, Coordinator/ALB handover, worker drain, result-retention checks, exact WireGuard peer removal, and protected API deletion. Add a one-Cube roundtrip for provision/setup/routing and verified scale-down.
- Keep the IONOS inventory and capacity planner read-only; use the dedicated fleet lifecycle commands for cloud mutations.
- Store secret values in the DLP L1 goldens (
dlp_l1.jsonl) only as generator recipes ({"gen", "seed"}) plus a<<secret:N>>carrier template. Python (patronus_ark.dlp_synth.recipes) and Rust (patronus_ark::golden_recipes,test-utilonly) materialize identical text from the shareddlp_synth/generators.jsonwith SplitMix64, checked by a fingerprint parity test. The 24 secret positives are migrated with an unchanged13_l1_golden_evalresult (69/69, 46/46), and the checked-in file no longer contains checksum-valid tokens or any default DLP finding. No detection change. - Add the L1-DLP benign-code corpus (
scripts/l1_dlp/benign_code.py) for FP/MB. It uses 16 permissively licensed GitHub repositories, each pinned to a commit with its license and corpus SHA-256 inbenign_code_repos.json, and leaves out secret-scanner repositories. Repositories are split 8/8 into dev and holdout. Each split covers code, lockfiles, minified JS, SVG, base64 and git logs. Key and credential files are skipped.preparewrites frozen split directories with a SHA-256 manifest, andfp-per-mbreports FP/MB in total and per kind, repository and rule. The data stays on the VPS. Detection change: none; this is evaluation tooling only. - Add the L1-DLP benign-document corpus (
scripts/l1_dlp/benign_docs.py) for false positives of content and document classes on German and English text. It has 12 sources inbenign_docs_sources.json: Wikipedia articles (prose), German federal laws frombundestag/gesetzeand 18F/GSA guides (government), and Wikimedia Foundation policies, legal pages and board minutes in German translation and English original (corporate). Each source is public domain or CC BY-SA, pinned to a git commit or MediaWiki revision ids, and has a corpus SHA-256. The split is by source; each split covers every language/genre pair, and no document or identical text is in both splits. Git sources fetch only the selected blobs, and MediaWiki revisions are checked against their titles.preparewrites frozen split directories with a manifest of license and SHA-256 per source, andfp-reportreports FP/MB and the flagged-document rate per rule, language, genre and source. The data stays on the VPS. Detection change: none; this is evaluation tooling only. - Add the L1-DLP baseline runner (
scripts/l1_dlp/run_baselines.py) for gitleaks 8.30.1, TruffleHog 3.97.9 (--no-verification) and detect-secrets 1.5.0. It checks versions and binary SHA-256 against pins, runs each tool in its default profile on one file per row (named after the row path or carrier format), and writes one prediction format: code-point value spans with the native rule id and entities/groups fromresearch/dlp/entity_map.yaml, no secret text, and a timestamp-free metadata sidecar. The per-tool normalization to the value span (gitleaks byte columns, TruffleHogRaw, detect-secrets prefix-only captures, URL userinfo, PEM blocks, escaped strings) is documented inscripts/l1_dlp/README.md.check-entity-mapchecks TruffleHog detector and detect-secrets plugin names against the pinned tools; the entity map now also maps TruffleHogXAI(xAI keys areshared) and the connection-string detectors tocredential.url_userinfo. Detection change: none; this is evaluation tooling only. - Add the L1-DLP ARK runner (
scripts/l1_dlp/run_ark.py). It scans the same JSONL as the baseline runner through the Python bindings (SecurityGateway(categories=["dlp"], max_level="l1"), default rule profile, optional--enable-rulefor default-off content rules) and writes the baseline prediction format: code-point value spans with thedlp_*rule id,ark.<rule_id>label and entities/groups fromresearch/dlp/entity_map.yaml, plus the engine group and span score. The rule id is recovered from thematched_rulesdetails of the scanner result that produced the span. A timestamp-free metadata sidecar pins the native build and counts spans per rule and group, unmapped rules and group mismatches. Detection change: none; this is evaluation tooling only. - Add the L1-DLP latency and throughput benchmark (
scripts/l1_dlp/latency.py).benchtimes ARK against gitleaks 8.30.1, TruffleHog 3.97.9 and detect-secrets 1.5.0 on four workloads built deterministically from the frozen dev split: single carrier rows, documents of exactly 4 KiB and 100 KiB (benign code with one carrier row per 4 KiB block) and a 10 MiB repository of benign-code files with carrier files. ARK is timed in process per document (p50/p95/p99) in a worker that loads a given build. The pinned baselines are timed as processes: start on an empty file, one run over every workload (scan and end-to-end MiB/s) and one invocation per document. detect-secrets runs single-process per file, because its CLI aborts on a file one plugin raises on. All processes are pinned to the same CPU, and reports carry numbers only. The scoreboard gate now times the head build against its base on the dev workloads in alternating rounds. It fails if the 100 KiB p95 rises more than 10 % (+0.25 ms slack) in two measurements, or if the head exceeds the H03 budget of 10 ms that the base keeps. The report is written tolatency.json. Detection change: none; this is evaluation tooling only. - Move the DLP L1 goldens (
dlp_l1.jsonl) to golden schema v2. Every row names thedlp_*rule it exercises (rule_id), its carrier document (carrier), difficulty tags (carrier,no_context,encoded,split,prose) and whether it is aknown_gap, a positive the detector misses today. Recipe placeholders can now encode the value (<<secret:N|base64>>,hex,url,json) or split it into ordered slices (<<secret:N[a:b]>>, one entity per fragment); Python (recipes.materialize_fragments) and Rust (golden_recipes::materialize_fragments) produce identical text. The set grows from 115 to 537 rows: at least 5 gated positives and 5 hard negatives per label and language (DE/EN), every one of the 59dlp_*rules with a gated positive in both languages and a hard negative, and 27 known gaps (encoded and split secrets, anchors followed byist/is/lautetor other words, quoted JSON keys, bare values without context).13_l1_golden_evalalso requires that the declared rule reported each gold span, prints coverage perrule_idand per tag, and fails if a rule loses its last passing positive in a language or a known gap starts passing. The checked-in file writes the placeholder colon as\u003a, so it stays free of default DLP findings. Detection change: none; this is evaluation tooling only. - Add the external DLP content goldens to the L1-DLP scoreboard (
scripts/l1_dlp/external_content.py). The dev-only componentexternal/holds the 214 Go and 13 Markdown files of gitleaks (source-code document labels; the Go files are also SQL negatives) and the SchemaPile-Perm SQL files with their first 250 statements as exact evidence spans. Both are normalized from the pinned sources and pinned by SHA-256, and the component is frozen like the other corpora. The contract indocs/research/dlp-external-content-contract-0.1.8.mddefines when a document label attaches: a judged class needs an in-scope evidence span. The scope of a SQL file ends after its last selected statement, so SchemaPile precision and the exact-span score can now be reported (exact F1 88.4 %, recall 79.2 %, precision 100 %). The scoreboard scores the component in the extended profile and fails on a loss of more than 0.5 pp in external document F1 or SQL span F1. It refuses the component in a holdout split. The runner creates it once with the new gate stepexternal-corpora.scripts/measure_external_dlp_runtime.pyis replaced. Detection change: none; this is evaluation tooling only. - Add the L1-DLP scoreboard (
scripts/l1_dlp/scoreboard.py).gate --head WT --base-sha SHA --dev DIR --holdout DIR --cache DIRbuilds the release module of the head and the base commit offline into the cache (one build per hash of the detector sources), predicts withrun_ark.pyin the default profile and in an extended profile with every opt-in rule, and scores both frozen splits: exact and overlap P/R/F1 on value spans per entity group, untyped and per provider, provider attribution, FP per hard-negative variant, document-level P/R/F1 per content class, FP/MB on benign code and FP/MB plus flagged documents on benign documents. It exits 1 if a group, the untyped score or a content class loses more than 0.5 pp F1, a benign corpus gets more FP, or a hard negative gets a new FP; errors exit 2. stdout carries dev numbers only, the holdout appears as a verdict and its report stays in the cache.scoreandcomparescore any tool's predictions in the shared format and compare two reports. Detection change: none; this is evaluation tooling only. - Add
scripts/l1_dlp/prepare_corpora.py, the gate step that freezes the L1-DLP dev and holdout corpora (--dev-out DIR --holdout-out DIR). Each split gets the benign-code corpus, the DE/EN benign-document corpus and synthetic carrier rows from its own template families. Dev uses a public generator seed; the holdout uses a secret seed created once asHOLDOUT/.seed(mode 0600) and recorded only as a SHA-256 commitment. Every component has a SHA-256 manifest; the top-level manifest pins them and the files, and.readyis written last. A frozen directory is only rewritten with--force, and--check DIRverifies it. The script needs only the standard library and git, so it runs in the baseline venv without the native module. Detection change: none; this is evaluation tooling only. - Add DLP carrier templates (
patronus_ark.dlp_synth.carriers): 80 families of.env, YAML, JSON, TOML, HCL, XML, properties/INI, shell/CLI, Dockerfile, CI YAML, Jupyter, log, HTTP, code in eight languages and DE/EN chat, ticket and mail documents, split into disjoint dev and holdout families. Positives have code-point spans on the value only, with the applied escapes recorded. Every family also yields placeholder, env/template reference, documented example value and test key hard negatives; each split has every format in DE and EN, and encoded example and test values keep their detector shape (Base58Check prefix,eyJJWT segments, PEM bodies) while only checksum or decoded structure is broken. A CLI writes the rows for one split and seed. No detection change. - Add the synthetic DLP secret generator (
patronus_ark.dlp_synth): deterministic, seedable generators for every secret and identifierdlp_*rule, including CRC-32-valid GitHub/npm tokens, Base58Check WIF keys, DER-parseable PEM keys (PKCS#8 Ed25519, PKCS#1, SEC1, DSA, OpenSSH), JSON-valid JWTs and ISO 7064-valid USt-IdNr. values.dlp_synth/rules.jsonmaps each rule to its generators and probe carriers. Python and Rust (golden_recipes,test-utilonly) share the extended spec (version 2), and the output is pinned by fingerprint vectors. Tests check every value with independent offline structure checks and against its rule. No detection change. - Add the DLP entity map (
research/dlp/entity_map.yaml) that maps everydlp_*rule to gitleaks 8.30.1 rule IDs, TruffleHog detectors, detect-secrets 1.5.0 plugins and Google Cloud DLP infoTypes withshared/ark_only/baseline_only/out_of_scopescopes, enforced by a Rust test. No detection change. - Add read-only IONOS Cube inventory and fleet capacity planning with protected-resource checks, identity validation, and redirect-safe credential handling. Provisioning and deletion remain explicitly unsupported.
Fixed¶
- Require bounded payment or employment context for ambiguous PII L1 expiry and employee-ID fields, and explicit bank-field syntax for SWIFT/BIC matches. Detection change: bare expiry dates, generic product IDs, and Swift-language prose no longer emit these PII findings.
- PII L1
pii_usernameno longer treats the word after "login", "username" or "Benutzer" as an account name unless it follows an explicit assignment (:,=,#, "is", "ist", "lautet"). Prose such as "CLI login token" or "login-flow" and common UI words ("Username: required") are no longer PII findings. Detection change: unassigned forms such as "Login max.mustermann" are no longer L1 findings. - Make IONOS fleet scale-down and round-trip recovery handle nested NIC inventory, provider target reordering, and the dynamically managed Coordinator WireGuard peer.
- Resume fleet changes safely after a provider accepted a target update before the local journal advanced, and use the test Cube's verified SSH host-key alias during drain.
- Persist the scale-up baseline before provisioning, reconcile accepted IONOS operations on resume, and make Coordinator/ALB registration idempotent across interrupted substeps.
- Replace deployment-specific names in the public IONOS profile example with explicit placeholders.
[0.1.8] - 2026-09-20¶
Added¶
- Added request-local classifier chunk overlap through the Ark API and Python queue API.
chunk_overlap_tokensdefaults to zero, is bounded to 64 tokens, and is applied consistently to shared L2/L3 chunks and decision-cache identity. - Added direct L3 classifier execution when L2 is disabled, including canonical model inputs, request-local gates, independent tool-tag properties, and calibrated final decisions without fabricated L2 evidence.
Changed¶
- Hardened Ark API and Coordinator request admission, ownership checks, event-stream access, worker recovery fencing, and request-local configuration snapshots.
- Split the Ark API entrypoint event handling, Unified L3 cache-key construction, and Python benchmark reporting into focused reusable modules while preserving their public behavior.
- Updated public documentation for category/level selection, offline requirements, deployment, result schemas, and the current API defaults.
Fixed¶
- Preserve detector result events in the Ark API entrypoint after the event-module extraction.
- Require L3 model readiness when a request enables direct L3 while L1 and L2 are disabled.
- Preserve the pre-0.1.8 chunking path exactly when
chunk_overlap_tokensis omitted or zero, including across internal 128-KiB tokenizer windows.
[0.1.7] - 2026-09-13¶
Added¶
- Added optional remote Unified L3 inference through a private Triton endpoint, with bounded HTTP requests, strict tensor validation, and pinned TensorRT deployment assets.
- Added bounded entrypoint overlap through
gateway.max_inflight_per_worker(one by default, optionally two), allowing L1/L2 work while L3 is pending without releasing completion tracking or worker recovery fences. Documented controlled fleet benchmarks. - Restored CLI regression coverage for hidden system-prompt disclosure, supplied Base64 instructions with verified decoded override/leakage signals, and destination-first secret handoffs. Includes bilingual cases, benign controls, rule gates, and source offsets.
-
Restored governed prompt coverage for supplied encoded overrides, forged system personas, prior bypass agreements, cross-tool redirects, and explicit secret exfiltration.
-
Added bilingual native Threat L1 rules with stable gates, source-bound action/target evidence, bounded matching, and the existing Threat taxonomy. Coverage includes remote and decoded execution, credential and session exfiltration, privilege and persistence risks, unsafe deserialization, metadata SSRF, and selected code/configuration relationships.
-
Added DE/EN anti-refusal, policy-nullification, and memory-override Injection rules plus bounded Unicode-tag decoding with original-source evidence. Existing Injection scorer coefficients and operating point remain unchanged. New rules include positive, negative, gate, and offset regression coverage.
-
Added a Redis-backed
ark-coordinatorservice for Cube fleets, with bounded admission, fair adaptive batching, three concurrent slots per Cube, parent deadlines, and document-wide aggregation of category decisions and evidence offsets. Incomplete chunk/category coverage yields a degraded result rather than an allow decision. - Added the versioned
ark-distributed-protocolwire contract and authenticated worker batch inference endpoint, with prepared token-ID inputs, tokenizer/model/config fingerprints, per-chunk failures, and optional immediate unified L3 inference for promoted chunks. - Added digest-pinned API and coordinator image workflows, resumable Cube/coordinator bootstrap scripts, private runtime configuration, deployment smoke checks, and an entrypoint benchmark.
- Added shared external Redis configuration and persistent overlapping fleet API keys for rotation, with atomic keyring recovery and migration of the interim key storage format.
- Added
POST /v1/scan/syncto the worker and public multi-worker gateway for clients that need final per-category results and completion metadata in a single authenticated response.
Changed¶
-
Shared request-local lowercase/offset views, native windows and Unicode-tag decoding across native L1 detectors, and tokenized ordered catalog relations once per input. Added necessary-condition prefilters for four more Injection and five PII rules, retaining original regex captures, Unicode boundaries, validators and scores.
-
Replaced the broad literal prefilter for system-prompt extraction with a necessary relationship check, retaining the original Unicode-aware regex and exact captures.
-
Reduced native L1 text-preparation allocations for byte-length-preserving Unicode lowercase mappings and skipped action regex scans when their required local anchors are absent, preserving matching semantics and source evidence.
-
The entrypoint assigns requests to free healthy workers through a bounded waiting queue. Worker status and admission epochs fence delayed submissions; interrupted jobs quarantine a worker until an authenticated idle recovery check succeeds. Readiness now includes worker health.
- Deferred distributed model fingerprint hashing until it is needed and bound fingerprints to loaded asset files; local startup reuses compatible tokenizers and shared embeddings without repeatedly hashing large assets. Unified L3 reuses prepared NTDB chunks and cached outputs.
- Replaced sorting-based memory-cache eviction with constant-time recency updates and eviction. Similarity candidate selection is bounded; encrypted derived outputs that can no longer be read are discarded and recomputed, and retention cleanup removes their index links.
Fixed¶
- Reduced native PII L1 false positives by discarding sub-four-byte findings, rejecting unformatted numeric IDs and Unix timestamps as phone numbers, and accepting short CVV findings only alongside a Luhn-valid payment-card number.
- Build the Coordinator image against the complete workspace lockfile and validate the Docker build on pull requests before publishing deployment artifacts.
- Projected Unified tool-tag sigmoid outputs into independent binary property results, preserving property-specific chunk aggregation and joint L2/L3 decision context.
- Include the new workspace crates in the API Docker build and trigger image validation for distributed-protocol changes, so Cargo can resolve the complete workspace in the container.
- Added the coordinator and protocol test suites to CI and release validation, and build the entrypoint binary required by the Python integration tests. Added license-check exceptions for the two first-party GPL crates and resolved Clippy errors in distributed gate planning and the entrypoint. Refreshed generated Rust API docs and deployment documentation for the new worker scheduling behavior.
- Aligned package versions, image labels, release metadata, and bootstrap image checks at 0.1.7.
Breaking¶
- Unified L3 tool-tag outputs are now exposed as independent binary property results rather
than a mutually exclusive multiclass result. Consumers of tool-tag results must handle one
presentorabsentresult per configured property and its property-specific L2/L3 context. - Classifier tokenization now requires NTDB v4 and compact mmBERT. Disjoint 128-KiB UTF-8 windows are tokenized once into shared 254-content-token chunks; L3 reuses those IDs with BOS/EOS and padding to 256 positions. Removed v2 execution, Hugging Face runtime fallbacks, and L3 re-tokenization. Chunk offsets track the source text and exact caches key token IDs. Existing v4 exports declaring 256 content tokens now use the 254-token runtime budget; this changes L2 chunk boundaries.
[0.1.6] - 2026-09-02¶
Added¶
- Added local-only external PII corpus normalization and exact-span evaluation contracts, plus a text-free, hash-bound workflow for human-verified internal PII annotations. Checked-in fixtures validate schemas and metrics only; no external or private raw corpus is distributed. Pinned OpenPII-Nano, TAB, and Apache-2.0 Gretel Finance adapters establish a reproducible external gold inventory and an OpenPII native-L1 baseline split by language, scope, and entity. Deterministic, group-atomic selection manifests cap reports at 250 spans per corpus and metric class.
- Added a text-free local PII/DLP pre-annotation inventory builder for
v4.1and related controlled corpora. Machine findings and Anchor-only hard-negative candidates are HMAC-bound and remain explicitly non-Gold until human review. Added derived, permissively sourced DLP document/content adapters for Go source documents and SQL statement boundaries without treating them as secret annotations or already measured end-to-end span scores. - Added native PII and DLP L1 capabilities with validated exact spans, German and English
anchor-bound identifiers, written birth dates, source/config/SQL/log detection, and non-blocking
localized
layers[].details.l1_anchorscontext facts. Added deterministic capability goldens, hard negatives, validators, and 100 KiB latency/evaluation tools. - Added an implementation-blind bilingual demo benchmark covering 87 customer, personnel, and
source-code documents with 204 expected PII/DLP/semantic spans and 19 hard negatives. Its
augmented employee-identifier forms extend L1 with
PersNr, employee/personnel number, staff-code, snake-case, OCR-field, and structuredEMP-*variants while preserving existing customer, student, tax, and unanchored build-ID negatives. - Added stable per-rule execution gates through Rust, Python, and Ark API configuration. Missing
rule IDs inherit shared defaults; PII/DLP patterns use
pii_*/dlp_*IDs and Injection uses its existingark.injection.*IDs. - Added a version-pinned native injection rule catalog derived from selected Prompt Armor rules. Catalog findings expose stable Ark/upstream rule IDs, exact byte and character spans, source revision, licence, family, severity, and descriptions. The first catalog closes high-specificity override, instruction-leak, boundary, Markdown exfiltration, and ES/FR/PT gaps without importing Prompt Armor's runtime or thresholds.
- Replaced the legacy Boolean L1 recognition/localization path with direct source-bound components for all 18 native Injection families and the five DLP operation/MCP producers. Regex, lexical, ordered-token and structural rules share the evidence contract; public classifications are projections of those matches. PII/DLP findings retain anchor/value components, and MCP retains tool/argument evidence. Existing rule/model gate IDs are preserved.
- Added complete bilingual reachability fixtures for all 39 PII and 59 DLP regex rules, native Injection families and MCP policy entries. Filled German/English gaps in contextual government identifiers, credential fields, jailbreak modes and agentic relations. Technical formats remain language-neutral. Normalized UTF-8 offsets map back to source; decoded containers are marked explicitly when exact decoded-character mapping is unavailable.
- Preserve overlapping PII findings across different labels; deduplicate overlaps only within
the same label. Optional
explaingates bounded diagnostic context anchors, not detection or the source-bound components of matched rules. - Added four source-derived, high-precision injection relationships for sensitive-path exfiltration, authority-issued replacement instructions, decode-then-execute directives, and delimited replacement actions. Each finding retains its pinned Pipelock or PromptInject source and any secondary Garak reference. Labeled ROT13 and URL-safe Base64 payloads are decoded before the existing injection-signal evaluation.
- Added the injection
L1Candidatecontract. Positive rule signals become auditablerule_matchfeatures; overlapping or directly touching spans form one deterministic candidate and separated regions remain separate. Candidates expose spans, rule IDs, families, severity, explanations, provenance and the versioned calibrated L1 score. - Added a native structural injection producer for override-plus-sensitive-disclosure relationships. It can create a candidate independently of the flat rule catalog and decomposes the decisive region into exact context-override, hierarchy-reference, disclosure-action, and sensitive-object features. Added German adaptations and nearby benign counterexamples for every new 0.1.6 catalog relationship; language-neutral Markdown exfiltration syntax is shared.
- Added a precision-first, monotone Injection L1 scorer calibrated on deterministic
injection_currentsamples and the complete Hard-Benign development splits. Its artifact, dataset hashes, feature order, threshold, candidate/document metrics and reproducible tooling are versioned in the repository; the final holdout remains a separate release gate.
Changed¶
- Injection L1 catalogs now scan their compiled regexes directly, removing the costly aggregate-regex prefilter while preserving findings, source-bound components, and provenance.
- Four expensive Injection context rules now skip impossible inputs using required-literal checks without Unicode word boundaries. Final matching retains Unicode case folding, word boundaries, captures, and original source offsets.
- Dynamic PII conditional labels now run as separate GLiNER inference groups scoped to chunks carrying the matching final source-pipeline class. Base labels stay stable, duplicate labels are removed from conditional calls, final L3 source classes supersede provisional L1/L2 classes, and duplicate entity spans keep the highest score across inference groups.
- The shared Rust/Python/Ark API DLP profile now enables credentials, secrets, authentication material, and sensitive-transfer context by default while gating broader business identifiers, metrics, source/SQL/dump/log content, MCP/runtime policy, and destructive operations behind explicit model/rule opt-ins.
- Dynamic PII now keeps overlapping hypotheses with different labels, reports all detected entity
types through evidence and label scores, uses additive context-label bundles, and provides
canonical
educationandmedicalmappings. Cross-text entity-cache matches no longer become live evidence; exact context-bound chunk caching remains enabled. - Added
PATRONUS_L3_TTL_SECS=-1to keep loaded L3 and Dynamic-PII ONNX sessions resident. The Ark API image and reference Compose deployment now use this setting to avoid the model reload latency spike on the first promoted request after an idle period. - Changed the reference and OVH Ark API deployment to two FP16 workers limited to 2.5 CPUs each. On the canonical cold-cache HTTP benchmark this improved throughput from 6.761 to 7.728 requests/s and reduced p50 latency from 440 ms to 168 ms versus three two-CPU workers. The Dockerfile now builds both API binaries explicitly and defaults its baked L3 assets to FP16.
- Changed native Injection L1 to publish one
native:injection_l1aggregate instead of separate public results for every heuristic. The former detector model gates still control their internal producers. Accepted candidates expose evidence spans anddecision.final_result.source: "l1"; rejected candidates keep their score and evidence without creating a finding and can be used by explicitly configured conditional L2/L3 gates. Registered external L1 detectors and DLP are unchanged.
Breaking¶
- Dynamic PII first-entity previews are now emitted as non-authoritative
provisionalevents instead of result-shaped events. Consumers must wait for the finalresultbefore acting. The obsoleteentity_cache_hit/entity_cache_hitsdiagnostics were removed with cross-text reuse. ScanGateMatrixhas a new publicrulesfield, and native PII/DLP layerdetailscan now containl1_anchors. External Rust struct literals and consumers that assumed empty native details must account for these intentional pre-1.0 additions.- Native Injection consumers that selected models such as
native:instruction_overrideornative:injection_rule_catalogfrom public scan results must migrate tonative:injection_l1and inspectlayers[].details.l1_candidates[].producers, rule provenance, and the typed decision candidates. This intentional pre-1.0 result-shape change ships with 0.1.6.
[0.1.5] - 2026-08-29¶
Added¶
- Added NTDB Package-v4/mmBERT loading for the exported
raw_text_to_joint_v3_chunk_promoter_union_v1contract: the neural stack and LightGBM promoter score each chunk independently, only promoted chunks run through L3, and document decisions use the export's L2-only, L3-only, and Union aggregation modes and thresholds. Manifest/model feature dimensions are validated at load time. Package-v2 runtime types remain available but are deprecated. - Added cache-backed Package-v4 parity evaluation for L2 probabilities, per-chunk promoter masks, and final document decisions, including multiclass and binary validation sets.
- Added pinned FP16 L3 selection via
PATRONUS_L3_PRECISION=fp16for Injection, Threat, Sensitive Document, and the Lion Warden unified model. - Added pinned FP16 Dynamic-PII/GLiNER selection. The image warmup now selects
onnx/fp16/model_fp16.onnxfrom the current revision of the Edge bundle whenPATRONUS_L3_PRECISION=fp16is set. - Added request-local Ark configuration to the existing multipart
POST /v1/scanendpoint. A request can override categories, maximum level, execution gates, metadata, and the NTDB operating point without restarting the API; omitted values retain the server/API-key defaults, and category overrides cannot exceed the worker or API-key scope. Existing text and multi-file uploads keep their original jobs contract. - Added
decision.decision_candidate.chunk_evidencefor Package-v4 Union decisions. It records the aggregation method, every contributing L2/L3 chunk, and the decisive chunk(s), so clients can attribute the final document verdict without reconstructing it from layer diagnostics. - Added
ark-apiYAML support for API-key default categories, Dynamic PII/GLiNER configuration, and the complete L3 scheduler policy (including per-pipeline overrides). Request-local gate policies use the same validated policy shape; invalid conditional gates and invalid scheduler bounds are rejected at configuration load time. - Added native and HTTP throughput regression tools for the production-style gated injection/DLP/threat profile using unified L3. The native benchmark runs one Ark with parallel requests for at least 60 seconds, reports progress while consuming events, and uses a mixed workload averaging 100 KiB per request (96 KiB median) with repeated and varied content, small 2–26 KiB requests, and 1 MiB spikes. It reports MiB/s, RPS, and latency percentiles and verifies completed requests, executed pipelines, levels, and the L3 model; Dynamic PII/GLiNER remains excluded because it requires a separate representative request-size profile.
- Added a CI throughput job that builds and installs a fresh release wheel, caches downloaded model assets, runs the gated unified benchmark without GLiNER, and publishes its JSON report.
- Added validated
pipeline.onnx_runtimeCPU session settings toark-api. Intra-op/inter-op thread counts and spinning are applied before startup warmup, so every ONNX session uses the deployment's bounded thread policy; zero thread counts and unknown fields are rejected. - Added explicit category and NTDB operating-point controls to the HTTP throughput benchmark so
full injection/DLP/threat, no-threat, and
best_promoteruns record the configuration they test.
Changed¶
- Re-pinned the Injection Package-v4/L3 bundle to Wolf Defender Small v2 and the three Tool Tags Package-v4 bundles to the current Husky Nose revision. Both model families now use the same immutable upstream revision for every runtime asset they supply.
- Pinned all nine published Package-v4/mmBERT L2 bundles (Injection, Sensitive Document, Tool
Class, Tool Action, the three split Tool Tags, Routing, and Threat) to their immutable Hugging
Face revisions. The unified L3 strategy uses the pinned public Lion Warden bundle, including
FP16 selection; the former
-edgerepositories are no longer referenced. - Changed Package-v4 routing to promote exclusively per chunk.
best_promoteuses the exported 25% calibration operating point, whilebest_f1andbest_latency_in_f1use the exported utility operating point. Post-L3 benefit and document-decider state are no longer part of the runtime contract; promoted chunks replace their L2 probabilities with L3 probabilities and non-promoted chunks retain L2 for export-defined Union aggregation. - Changed Joint-v3 ONNX execution to the export's exact per-chunk tensor contract across Injection, Threat, Sensitive Document, Routing, Tool Class, Tool Action, and the split Tool Tags pipelines. Independent chunks remain parallelizable without document padding or attention-mask changes.
- Preserved the existing L3 strategy engine for Package-v4 promotions, including request-wide Early Exit, Representatives, Verify Representative, clustering, Exact Cache, and similarity propagation. Package-v4 changes which chunks are promoted, not how L3 schedules them.
- Changed Package-v4 final arbitration to use the export's default Union view while retaining
L2-only, L3-only, and Union decision candidates for policy inspection. A rejected Union candidate
returns the default class and is not overridden by an accepted L2-only candidate. Package-v2
keeps its existing
decision_thresholds.jsonarbitration unchanged. - Changed the
ark-apideployment to use the shared Unified L3 classifier for regular model heads and the separate Dynamic PII/GLiNER runtime. The Docker build bakes both asset bundles, and the reference build configuration now enables the production Injection/DLP/PII/Sensitive Document/ Threat/Routing/Dynamic PII profile at L3 with one request worker per container. - Changed
ark-apiscan input handling to accept bothtextandcontentmultipart fields; request-local configuration is resolved once and snapshotted into each text or file job. -
Changed completed
ark-apiSSE request retention from five minutes to one minute. -
Optimized the CPU inference setup by updating the default ONNX Runtime Rust bindings to
ort2.0.0-rc.13/ ONNX Runtime 1.28 and making execution-provider selection feature-aware: accelerator builds prefer their configured provider and fall back to CPU, while CPU-only builds select the optimized CPU execution path directly instead of probing unavailable accelerators. - Changed unified-L3 warmup to execute a real inference after loading the session, so Docker build warmup prepares both the embedded model assets and the runtime's first-inference path.
- Extended API readiness output with the available and actually active ONNX execution providers.
Removed¶
- Removed NTDB
.kittokenizer loading and generation. Official Package-v4 models use the mmBERT-specific.mmbperuntime, withtokenizer.jsonas the fallback.
Fixed¶
- Fixed public gateway job polling to retain worker
evidence_spansfor every category. Native PII/DLP and Dynamic-PII consumers now receive labels, matched text, score, and byte/character offsets needed for redaction alongside classifierdecision_evidence. - Fixed x86_64 CPU inference parity for the verified German greeting and Dynamic-PII person-span
cases by using the pinned FP16 ONNX graphs in production. On x86_64 deployments, build and run
the API with
PATRONUS_L3_PRECISION=fp16; the default quantized graphs remain suitable only where their output has been validated for the target runtime. -
Fixed Dynamic PII asset selection so a pinned FP16 GLiNER model is downloaded, baked, and loaded as one coherent bundle instead of validating it against the quantized model manifest.
-
Updated Lion Warden, Wolf Defender Injection, Wolf Defender Threat, and Orca Sonar L3 assets to their public non-
-edgeHugging Face repositories and current immutable revisions. FP16 selection now uses the repositories'onnx/onnx_fp16/model_fp16.onnxlayout, and Lion Warden's Sensitive Document head accepts its neweducationandmedicalclasses. - Fixed automatic Package-v4 model updates so the downloader parses freshly fetched manifests and the warmup validator through the Package-v4-aware parser before resolving or loading their runtime artifacts, including exported non-finite report-only metric values.
- Fixed warmup revision enforcement for official L2 packages: an existing manifest no longer
suppresses an update when its
.patronus-revisionmarker is absent or stale, or when its shared embedding matrix does not match the pinned manifest dimensions. Explicit local package overrides remain untouched. - Fixed shared-embedder cache migration across encoder generations: embedding matrices with a mismatched manifest size are no longer reused, and asset downloads replace hard links or symlinks atomically instead of truncating another package's shared cache inode.
- Fixed an
ark-apievent-delivery race: scans that emit before the HTTP handler registers their request buffer now create that buffer in the dispatcher, preserving the event history for the subsequent SSE subscriber. -
Clarified that the built-in benchmark corpus contains synthetic historical regression fixtures, not the model-release validation splits; its F1 values are therefore not release-validation metrics.
-
Prevented CPU-only builds from attempting to register an accelerator execution provider that was not compiled into the binary.
Breaking¶
- Completed
ark-apiscan event streams are retained for one minute instead of five. Clients must begin consumingGET /v1/scan/{request_id}/eventswithin that shorter window.
[0.1.4] - 2026-08-18¶
Added¶
- Added the public
PrivateNetworkDiagnosticHook, which retains private and local IP predictions for diagnostics while filtering their evidence spans from network-diagnostic output. - Added shared tokenizer caching and direct mmBERT token-ID handoff from compatible NTDB L2 chunks to dedicated and unified L3 inference.
- Added
ark-api, an ephemeral HTTP API around the security pipeline for self-hosted and Patronus-hosted deployment:POST /v1/scanaccepts text and/or one or more files and returns arequest_idper input,GET /v1/scan/{request_id}/eventsstreams progress and per-category results over Server-Sent Events, buffering events so a client that only starts polling after a fast scan completes still sees the full history instead of a 404. - Added YAML-based
ark-apiconfiguration, including per-API-key category scoping and gate overrides:pipeline.gates(and per-keyauth.keys[].gates) deserialize directly intoConditionalPipelineGate/GateExpression, so the full metadata/prior-result condition tree and per-pipeline L3 policy overrides are configurable in YAML without a bespoke parser. - Added a multi-stage
ark-apiDockerfile that bakes model assets in at build time (viaark-api --warmup-only) and adocker-compose.ymlreference deployment for self-hosting.
Changed¶
- Changed compatible mmBERT NTDB runtime chunks to 254 content tokens so L2 tokenization can be reused safely by 256-token L3 models with their boundary tokens.
- Tightened native injection, DLP, and MCP relationship matching so actionable verbs, targets, and destinations must occur in supported request forms.
- Expanded Unicode-confusable normalization so mixed-script security signals are evaluated through an ASCII skeleton before a finding is emitted.
Fixed¶
- Reduced false-positive native findings in source code, test names, logs, documentation, benign tool output, and ordinary developer-mode or network-diagnostic text.
- Prevented compatible L2 spans from being re-tokenized or silently truncated during L3 inference; incompatible or oversized handoffs now fall back to model-backed L3 rechunking.
- Allowed unified and dedicated L3 bundles to load a compact
tokenizer.mmbpewhentokenizer.jsonis unavailable, while preserving the JSON fallback. - Worked around a
serde_yaml0.9 bug deserializing recursive enums nested inside sequences (hit byark-api's conditional gate config) by routing YAML config parsing throughserde_json.
Breaking¶
- Native injection, DLP, MCP, obfuscation, and PII evidence decisions can change because matching relationships, Unicode-confusable handling, and post-prediction evidence filtering were updated.
- Compatible mmBERT NTDB packages now use 254 content tokens per runtime chunk instead of 256.
[0.1.3] - 2026-08-11¶
Added¶
- Added public post-prediction hooks for filtering non-actionable credential and local-path evidence while retaining model predictions for diagnostics.
- Added precision-first, real-corpus calibration data and a local sweep tool for general Dynamic PII labels, plus benchmark coverage for names, dates of birth, cities, and countries.
Changed¶
- Expanded the default and document-aware Dynamic PII label sets with calibrated
first_name,last_name,date_of_birth,city, andcountryentities, and recalibrateddate.
Fixed¶
- Reduced false-positive DLP and MCP findings for template
.envcopies, empty or placeholder secret assignments, and low-entropy credential values. - Suppressed
personevidence when a detected name is only a username inside a local filesystem path. - Rejected loopback, multicast, unspecified, and truncated IPv6 candidates as PII, and rejected terminal metadata words and invalid zero postal codes as German postal addresses.
Breaking¶
- Dynamic PII findings and native PII/DLP/MCP decisions can change because label sets, thresholds, evidence filtering, and native validators were updated.
[0.1.2] - 2026-07-26¶
Added¶
- Added compact
.mmbpetokenizer generation for compatible mmBERT byte-fallback BPE tokenizers during verified downloads and cached warmup. The originaltokenizer.jsonremains the canonical fallback. - Added German imperative variants to the native
instruction_overrideL1 detector.
Changed¶
- Added the structured
decisionenvelope to model-backed classifier results. The envelope exposes the final Ark verdict, the canonical policy candidate, all typed L2/L3/Union candidates, Ark's calibrated recommendation, terminality, and minimal provenance. - Restricted
decisionto terminal authoritative classifier results. Early L2 results withl3_pending, provisional events, and result-preview events now leavedecisionunset so downstream policy consumers can key onresult.decision.is_some(). - Extended compact tokenizer asset preparation beyond Granite
.kitgeneration so supported mmBERT L2/L3 bundles can reuse hash- and version-invalidated generated artifacts. - Changed classifier default arbitration to preserve a calibrated default-class confidence when
the producing model exposes one, instead of always forcing
0.0.
Fixed¶
- Fixed promoted NTDB L2 fallback results so the final-decision threshold profile is still applied before publishing the fallback class.
- Fixed L2-only classifier arbitration so accepted candidates are selected from model label scores instead of the already defaulted top-level result.
- Fixed persistent
redbcache handling to recreate a missing or externally deleted database file while still surfacing corrupt databases and active second-writer conflicts as errors.
Breaking¶
- Removed the redundant NTDB L2
details.raw_class_nameanddetails.raw_confidencefields. Consumers should readdecision.decision_candidateanddecision.candidates[]instead. - Candidate arbitration data is no longer exposed as public
layers[].details.arbitration_*fields. The public contract is the top-leveldecisionenvelope.
[0.1.1] - 2026-07-26¶
Added¶
- Added bundled NTDB final-decision thresholds for L2, L3, and weighted L2/L3 union arbitration across classifier pipelines.
- Added request-local
enqueue(..., ntdb_operating_point=...)support for overriding the final-decision threshold profile on queued scans. - Added
threatvalidation samples to the built-in local benchmark.
Changed¶
- Changed classifier final arbitration to accept L3 first, then a weighted L2/L3 union, then L2, and otherwise return the pipeline default class.
- Changed Python's
ntdb_operating_pointmeaning to select the final-decision threshold profile; L2 promotion continues to use the NTDB package promote operating point and is not changed by that Python setting. - Changed the default final-decision threshold profile to
best_f1.
Removed¶
- Removed
tool_classvalidation samples from the built-in local benchmark fixture set.
Breaking¶
- Classifier result decisions can change because calibrated final-decision thresholds now apply to L2, L3, and union arbitration.
- Benchmark comparisons against previous local runs are not one-to-one for
tool_class, because the packaged benchmark fixture was removed andthreatwas added.
Added¶
- Added
normalize_text(text, configs={})as a pure text-normalization API for applyingcanonical_security_text_v1before scanning or for direct caller use.
Fixed¶
- Added a separate macOS Intel wheel build that pins
ortto2.0.0-rc.10, which still provides prebuilt ONNX Runtime binaries forx86_64-apple-darwin. - Kept macOS arm64, Linux, and Windows wheel builds on
ort2.0.0-rc.12.
[0.1.0] - 2026-07-24¶
Initial public release of Patronus Ark.
Added¶
- Hybrid Rust/Python security scanning library published as the
patronus-arkRust crate and Python package. SecurityGatewayfor synchronous scans and queued request processing.- Scan categories for prompt injection, DLP, PII, dynamic PII, sensitive documents, tool classification, tool actions, tool tags, routing, and threat detection.
- Layered scanning with native L1 detectors, NTDB L2 model packages, and promoted L3 ONNX models.
- Configurable model asset download and cache management.
- Configurable execution gates, L3 scheduling policy, L3 strategy selection, and ONNX backend options.
- Hot and persistent result caching.
- Built-in local benchmark runner with packaged benchmark fixtures.
- Rust and Python examples plus documentation for installation, quickstart, configuration, assets, result schema, and release/testing workflows.
Notes¶
This is the first public changelogged release. Future releases will record Added / Changed / Deprecated / Removed / Fixed / Security sections here.