active_sanction 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +538 -0
  3. data/CODE_OF_CONDUCT.md +74 -0
  4. data/CONTRIBUTING.md +312 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +616 -0
  7. data/SECURITY.md +97 -0
  8. data/active_sanction.gemspec +98 -0
  9. data/docs/adding_a_source.md +1083 -0
  10. data/docs/api_stability.md +357 -0
  11. data/docs/bundle_format.md +407 -0
  12. data/lib/active_sanction/address.rb +170 -0
  13. data/lib/active_sanction/client.rb +341 -0
  14. data/lib/active_sanction/configuration.rb +721 -0
  15. data/lib/active_sanction/countries.txt +292 -0
  16. data/lib/active_sanction/country.rb +164 -0
  17. data/lib/active_sanction/deprecation.rb +168 -0
  18. data/lib/active_sanction/diff/change.rb +208 -0
  19. data/lib/active_sanction/diff.rb +348 -0
  20. data/lib/active_sanction/doctor/checkup.rb +356 -0
  21. data/lib/active_sanction/doctor/diagnosis.rb +245 -0
  22. data/lib/active_sanction/doctor/finding.rb +183 -0
  23. data/lib/active_sanction/doctor/profile.rb +384 -0
  24. data/lib/active_sanction/doctor/report.rb +227 -0
  25. data/lib/active_sanction/doctor.rb +347 -0
  26. data/lib/active_sanction/entity.rb +293 -0
  27. data/lib/active_sanction/error.rb +332 -0
  28. data/lib/active_sanction/fetcher/result.rb +116 -0
  29. data/lib/active_sanction/fetcher.rb +242 -0
  30. data/lib/active_sanction/http_client/errors.rb +94 -0
  31. data/lib/active_sanction/http_client/response.rb +129 -0
  32. data/lib/active_sanction/http_client.rb +324 -0
  33. data/lib/active_sanction/identifier.rb +212 -0
  34. data/lib/active_sanction/index/builder.rb +89 -0
  35. data/lib/active_sanction/index/candidate.rb +63 -0
  36. data/lib/active_sanction/index/entry.rb +66 -0
  37. data/lib/active_sanction/index/features.rb +112 -0
  38. data/lib/active_sanction/index.rb +381 -0
  39. data/lib/active_sanction/match_result.rb +345 -0
  40. data/lib/active_sanction/matcher.rb +388 -0
  41. data/lib/active_sanction/name.rb +183 -0
  42. data/lib/active_sanction/normalizer/cache.rb +82 -0
  43. data/lib/active_sanction/normalizer/dictionaries/honorifics.txt +30 -0
  44. data/lib/active_sanction/normalizer/dictionaries/legal_forms.txt +72 -0
  45. data/lib/active_sanction/normalizer/dictionaries/organization_stopwords.txt +20 -0
  46. data/lib/active_sanction/normalizer/dictionaries/particles.txt +35 -0
  47. data/lib/active_sanction/normalizer/dictionary/stoplist.rb +121 -0
  48. data/lib/active_sanction/normalizer/dictionary.rb +243 -0
  49. data/lib/active_sanction/normalizer/form.rb +279 -0
  50. data/lib/active_sanction/normalizer.rb +137 -0
  51. data/lib/active_sanction/parsers/column_shape.rb +274 -0
  52. data/lib/active_sanction/parsers/delimited_table/reader.rb +194 -0
  53. data/lib/active_sanction/parsers/delimited_table/row.rb +81 -0
  54. data/lib/active_sanction/parsers/delimited_table.rb +151 -0
  55. data/lib/active_sanction/parsers/format.rb +107 -0
  56. data/lib/active_sanction/parsers/join.rb +149 -0
  57. data/lib/active_sanction/parsers/spreadsheet/archive.rb +258 -0
  58. data/lib/active_sanction/parsers/spreadsheet/reader.rb +213 -0
  59. data/lib/active_sanction/parsers/spreadsheet/row.rb +88 -0
  60. data/lib/active_sanction/parsers/spreadsheet/workbook.rb +314 -0
  61. data/lib/active_sanction/parsers/spreadsheet.rb +168 -0
  62. data/lib/active_sanction/parsers/xml_records/backends/nokogiri.rb +134 -0
  63. data/lib/active_sanction/parsers/xml_records/backends/rexml.rb +157 -0
  64. data/lib/active_sanction/parsers/xml_records/backends.rb +103 -0
  65. data/lib/active_sanction/parsers/xml_records/builder.rb +90 -0
  66. data/lib/active_sanction/parsers/xml_records/reader.rb +134 -0
  67. data/lib/active_sanction/parsers/xml_records/record.rb +174 -0
  68. data/lib/active_sanction/parsers/xml_records.rb +140 -0
  69. data/lib/active_sanction/parsers.rb +94 -0
  70. data/lib/active_sanction/partial_date/parser.rb +124 -0
  71. data/lib/active_sanction/partial_date.rb +303 -0
  72. data/lib/active_sanction/payload_cache/checksum.rb +66 -0
  73. data/lib/active_sanction/payload_cache/entry.rb +304 -0
  74. data/lib/active_sanction/payload_cache.rb +366 -0
  75. data/lib/active_sanction/phonetics/double_metaphone.rb +662 -0
  76. data/lib/active_sanction/phonetics.rb +40 -0
  77. data/lib/active_sanction/query.rb +290 -0
  78. data/lib/active_sanction/rescreen/alert.rb +314 -0
  79. data/lib/active_sanction/rescreen.rb +332 -0
  80. data/lib/active_sanction/scorer/adjustments.rb +241 -0
  81. data/lib/active_sanction/scorer/name_score.rb +279 -0
  82. data/lib/active_sanction/scorer/reason.rb +136 -0
  83. data/lib/active_sanction/scorer/result.rb +129 -0
  84. data/lib/active_sanction/scorer/subject.rb +235 -0
  85. data/lib/active_sanction/scorer/weights.rb +306 -0
  86. data/lib/active_sanction/scorer.rb +303 -0
  87. data/lib/active_sanction/similarity/jaro_winkler.rb +210 -0
  88. data/lib/active_sanction/similarity/levenshtein.rb +179 -0
  89. data/lib/active_sanction/similarity/token_set.rb +159 -0
  90. data/lib/active_sanction/similarity/token_sort.rb +120 -0
  91. data/lib/active_sanction/similarity.rb +183 -0
  92. data/lib/active_sanction/snapshot/bundle/header.rb +302 -0
  93. data/lib/active_sanction/snapshot/bundle/payload.rb +185 -0
  94. data/lib/active_sanction/snapshot/bundle/signature.rb +201 -0
  95. data/lib/active_sanction/snapshot/bundle.rb +381 -0
  96. data/lib/active_sanction/snapshot.rb +305 -0
  97. data/lib/active_sanction/sources/australia_dfat/published_date.rb +166 -0
  98. data/lib/active_sanction/sources/australia_dfat/record.rb +319 -0
  99. data/lib/active_sanction/sources/australia_dfat.rb +291 -0
  100. data/lib/active_sanction/sources/base.rb +295 -0
  101. data/lib/active_sanction/sources/canada_sema/record.rb +282 -0
  102. data/lib/active_sanction/sources/canada_sema/source_ref.rb +89 -0
  103. data/lib/active_sanction/sources/canada_sema.rb +153 -0
  104. data/lib/active_sanction/sources/definition.rb +360 -0
  105. data/lib/active_sanction/sources/eu_fsf/record.rb +467 -0
  106. data/lib/active_sanction/sources/eu_fsf.rb +203 -0
  107. data/lib/active_sanction/sources/ofac/record.rb +251 -0
  108. data/lib/active_sanction/sources/ofac/remarks_parser/coverage.rb +120 -0
  109. data/lib/active_sanction/sources/ofac/remarks_parser/vocabulary.rb +121 -0
  110. data/lib/active_sanction/sources/ofac/remarks_parser.rb +312 -0
  111. data/lib/active_sanction/sources/ofac.rb +248 -0
  112. data/lib/active_sanction/sources/ofac_consolidated/record.rb +37 -0
  113. data/lib/active_sanction/sources/ofac_consolidated.rb +203 -0
  114. data/lib/active_sanction/sources/ofac_sdn.rb +43 -0
  115. data/lib/active_sanction/sources/remarks.rb +84 -0
  116. data/lib/active_sanction/sources/uk_sanctions_list/published_date.rb +104 -0
  117. data/lib/active_sanction/sources/uk_sanctions_list/record.rb +412 -0
  118. data/lib/active_sanction/sources/uk_sanctions_list.rb +214 -0
  119. data/lib/active_sanction/sources/un_consolidated/record.rb +279 -0
  120. data/lib/active_sanction/sources/un_consolidated.rb +140 -0
  121. data/lib/active_sanction/sources.rb +196 -0
  122. data/lib/active_sanction/storage/active_record/reader.rb +124 -0
  123. data/lib/active_sanction/storage/active_record/row.rb +175 -0
  124. data/lib/active_sanction/storage/active_record/writer.rb +141 -0
  125. data/lib/active_sanction/storage/active_record.rb +277 -0
  126. data/lib/active_sanction/storage/base.rb +216 -0
  127. data/lib/active_sanction/storage/file_system.rb +373 -0
  128. data/lib/active_sanction/storage/memory.rb +67 -0
  129. data/lib/active_sanction/storage/meta.rb +188 -0
  130. data/lib/active_sanction/storage.rb +74 -0
  131. data/lib/active_sanction/subject.rb +271 -0
  132. data/lib/active_sanction/sync/report.rb +245 -0
  133. data/lib/active_sanction/sync/result.rb +296 -0
  134. data/lib/active_sanction/sync.rb +390 -0
  135. data/lib/active_sanction/validator_store/file_system.rb +103 -0
  136. data/lib/active_sanction/validator_store/memory.rb +41 -0
  137. data/lib/active_sanction/validator_store.rb +119 -0
  138. data/lib/active_sanction/validators.rb +195 -0
  139. data/lib/active_sanction/version.rb +23 -0
  140. data/lib/active_sanction.rb +331 -0
  141. data/lib/generators/active_sanction/install/install_generator.rb +60 -0
  142. data/lib/generators/active_sanction/install/templates/create_active_sanction_tables.rb.tt +142 -0
  143. metadata +236 -0
data/SECURITY.md ADDED
@@ -0,0 +1,97 @@
1
+ # Security policy
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ **Report privately. Do not open a public issue.**
6
+
7
+ Use GitHub's private vulnerability reporting —
8
+ [**Report a vulnerability**](https://github.com/Babystep-Technologies/active_sanction/security/advisories/new)
9
+ on the Security tab — which opens a draft advisory only you and the maintainers
10
+ can read. If that is unavailable to you, email
11
+ **shen.marshall@gmail.com** with `active_sanction` in the subject.
12
+
13
+ Please include the version, the Ruby, the source list involved if there is one,
14
+ and the smallest input that reproduces it. A record from a published government
15
+ file is more useful than a synthetic one, and naming the record is not a
16
+ disclosure — it is already public.
17
+
18
+ ## What to expect
19
+
20
+ This project is maintained by one person, so these are commitments about
21
+ communication rather than about engineering capacity.
22
+
23
+ | | |
24
+ |---|---|
25
+ | Acknowledgement | within 3 business days |
26
+ | Initial assessment, in scope or not | within 10 business days |
27
+ | Status update while a fix is open | at least every 14 days |
28
+ | Coordinated disclosure window | 90 days by default, negotiable |
29
+
30
+ A fix ships in a release, with an advisory naming the versions affected and a
31
+ `CHANGELOG.md` entry. **You are credited by name unless you ask not to be.** If
32
+ a report turns out to be a plain bug rather than a vulnerability, we will say so
33
+ and move it to a public issue with your agreement.
34
+
35
+ ## What is in scope
36
+
37
+ **A screening library that fails to report a listed name is a security
38
+ problem, not merely an inaccuracy.** Somebody may be relying on an empty result
39
+ to clear a payment, and the liability for a missed designation is real. So the
40
+ scope here is wider than it would be for an ordinary library:
41
+
42
+ - **A systematic screening bypass.** A class of name, a script, a
43
+ transliteration or a crafted input that reliably evades matching — not one
44
+ fuzzy score you disagree with, but a shape of input that gets a whole
45
+ category of listed records past the matcher.
46
+ - **Integrity of a snapshot or a bundle.** Anything that makes the gem accept a
47
+ modified list as authentic: a signature that verifies when it should not, a
48
+ checksum that can be satisfied by altered content, a bundle that loads
49
+ without the verification the caller asked for.
50
+ - **Provenance of a stamped result.** A screening result carries the snapshot
51
+ checksum, the matcher version and the weights so that a decision can be
52
+ re-derived years later. Anything that lets a result carry a stamp that does
53
+ not describe what actually answered it is in scope.
54
+ - **The fetch path.** Redirect handling that can be steered somewhere the
55
+ publisher did not point, certificate or transport verification that can be
56
+ downgraded, a cached payload that can be poisoned.
57
+ - **Parsing a hostile file.** Denial of service, memory exhaustion, external
58
+ entity resolution or file disclosure triggered by a malformed or malicious
59
+ list, archive or bundle. A government endpoint is a third party, and one that
60
+ has been compromised serves this library its input.
61
+ - **The storage and cache paths.** Path traversal, a file written outside the
62
+ directory the host configured, or predictable temporary files.
63
+ - **Leaking a screened name.** Any path by which a subject's name, or the
64
+ details submitted with it, leaves the host process — a log line, an outbound
65
+ request, an error message. *No name you screen leaves your process* is a
66
+ promise this library makes, and a break in it is a vulnerability.
67
+
68
+ ## What is not in scope
69
+
70
+ - **A single missed record or false alert.** That is match quality. It belongs
71
+ in a public issue and in
72
+ [`benchmark/fixtures/labeled_set.yml`](benchmark/fixtures/labeled_set.yml),
73
+ where it becomes a regression test. The line between this and a systematic
74
+ bypass is whether the technique generalises; if you are unsure, report it
75
+ privately and we will sort it out.
76
+ - **A jurisdiction this gem does not cover,** or a list it does not read. The
77
+ README says which seven ship.
78
+ - **The absence of PEP data, adverse media, beneficial ownership or 50 Percent
79
+ Rule resolution.** These are documented non-goals, not gaps.
80
+ - **A publisher's own data being wrong,** or a government endpoint being down.
81
+ Upstream format changes are handled by the canary and belong in a public
82
+ issue.
83
+ - **Vulnerabilities in the commercial hosted service.** Report those to the
84
+ same address; they are not tracked in this repository.
85
+ - **Reports generated by a scanner with no working reproduction,** and findings
86
+ in development-only tooling that never ships in the gem.
87
+
88
+ ## Supported versions
89
+
90
+ Pre-1.0, only the most recent release receives fixes. There are no backports to
91
+ earlier 0.x versions. Once 1.0 ships, this section will state a support window.
92
+
93
+ ## Dependencies
94
+
95
+ The gem's runtime dependencies are `csv`, `rexml` and `sorbet-runtime`, all
96
+ pure Ruby. A vulnerability in one of those belongs upstream, but tell us anyway
97
+ if this library's use of it is what makes it reachable — that part is ours.
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/active_sanction/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "active_sanction"
7
+ spec.version = ActiveSanction::VERSION
8
+ spec.authors = ["Marshall Shen"]
9
+ spec.email = ["shen.marshall@gmail.com"]
10
+
11
+ spec.summary = "A Ruby toolkit that manages sanction lists around the world."
12
+ spec.description = "ActiveSanction fetches government sanctions lists, normalizes them into a single record model, persists them in preferred storage, and screens names against them with explainable fuzzy match scores. Every match carries structured reasons and stamps the snapshot checksum, matcher version, and thresholds so a screening decision can be re-derived during an audit. New sources register through public extension points without forking the gem."
13
+ spec.homepage = "https://github.com/Babystep-Technologies/active_sanction"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.1"
16
+
17
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = "https://github.com/Babystep-Technologies/active_sanction/tree/main"
21
+ spec.metadata["changelog_uri"] = "https://github.com/Babystep-Technologies/active_sanction/blob/main/CHANGELOG.md"
22
+ spec.metadata["rubygems_mfa_required"] = "true"
23
+
24
+ # `csv` and `rexml` are stdlib, not third parties: both shipped inside Ruby
25
+ # and have since been moved to bundled gems, which means they now have to be
26
+ # named to be requirable. Declaring them keeps the gem loadable without
27
+ # changing what it costs a user -- there is still nothing here to compile and
28
+ # nothing to vendor.
29
+ #
30
+ # Nokogiri is deliberately *not* declared. The XML toolkit can drive libxml2
31
+ # and will if an application asks it to, but a compliance library should not
32
+ # be the reason a deployment starts building native extensions.
33
+ spec.add_dependency "csv", "~> 3.3"
34
+ spec.add_dependency "rexml", "~> 3.3"
35
+
36
+ # Sorbet's runtime, and the only piece of the checker that ships. The `sig`
37
+ # blocks in `lib/` are ordinary method calls, so the library does not load
38
+ # without it. It is pure Ruby with nothing to compile, which is the rule the
39
+ # Nokogiri paragraph above is really about. The static half -- `sorbet` and
40
+ # `tapioca` -- stays in the Gemfile, where a host never sees it.
41
+ #
42
+ # It is not free at call time, so the rule is that a signature on a path
43
+ # which runs per query is declared `.checked(:tests)`: enforced by this
44
+ # gem's suite and inert in a host's process. The normalizer is the first
45
+ # such path and carries it throughout; the scorers join it as they land.
46
+ # A host that wants none of it at all can set
47
+ # `T::Configuration.default_checked_level = :never` before requiring the gem,
48
+ # which spec/sorbet_runtime_spec.rb holds us to.
49
+ spec.add_dependency "sorbet-runtime", "~> 0.6"
50
+
51
+ # What ships. Everything tracked in git, minus the parts of this repository
52
+ # that exist to develop it.
53
+ #
54
+ # `spec/` carries the fixtures, which are trimmed copies of government files
55
+ # and are the largest thing here. `sorbet/` is the checker's working
56
+ # directory -- its config and the RBIs tapioca generates for our
57
+ # dependencies. `benchmark/` measures the machine it runs on and answers a
58
+ # question about this repository, and `canary/` (#69) watches seven
59
+ # government endpoints on this repository's behalf -- both are here to keep
60
+ # the library honest, and neither does anything in an application that
61
+ # installed it. The rest is toolchain: CI, the linter's config, the Rakefile
62
+ # that drives all of them, and `bin/` -- none of which do anything inside an
63
+ # installed gem, and each of which is one more file a security scan has to be
64
+ # told to ignore.
65
+ #
66
+ # `site/` (#103) is the documentation site's sources -- an Astro project, a
67
+ # theme, a link checker and a `node_modules` the moment anybody builds it.
68
+ # It exists to be published at a URL, and an application that installed this
69
+ # gem has no use for the machinery that publishes it.
70
+ #
71
+ # `AGENTS.md` and `.claude/` (#113) are instructions for a coding agent
72
+ # working *on* this repository -- which adapter to copy, which gates to run,
73
+ # which commands reach a government endpoint and must not be run unattended.
74
+ # They are toolchain in the same sense the Rakefile is, and say nothing to an
75
+ # application that installed the gem. The leading-dot rule below already
76
+ # excludes `.claude/`; `AGENTS.md` is named because it sits at the root.
77
+ #
78
+ # What is deliberately kept is `docs/`, which is linked from the README and
79
+ # is as much a part of the library as the code is. That is the whole reason
80
+ # the site is not built out of `docs/`: doing so would ship an Astro config
81
+ # and a set of layouts into every application that installs this gem.
82
+ dev_only = %r{
83
+ \A(?:
84
+ (?:test|spec|features|sorbet|benchmark|canary|bin|site|\.github)/ |
85
+ Gemfile |
86
+ Rakefile |
87
+ AGENTS\.md |
88
+ \.
89
+ )
90
+ }x
91
+
92
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
93
+ `git ls-files -z`.split("\x0").reject { |f| f.match(dev_only) }
94
+ end
95
+ spec.bindir = "exe"
96
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
97
+ spec.require_paths = ["lib"]
98
+ end