homographic_spoofing 0.1.1 โ†’ 0.2.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.
@@ -388,6 +388,16 @@
388
388
  ๐ดท,hanifi_rohingya_digit
389
389
  ๐ดธ,hanifi_rohingya_digit
390
390
  ๐ดน,hanifi_rohingya_digit
391
+ ๐ต€,garay_digit
392
+ ๐ต,garay_digit
393
+ ๐ต‚,garay_digit
394
+ ๐ตƒ,garay_digit
395
+ ๐ต„,garay_digit
396
+ ๐ต…,garay_digit
397
+ ๐ต†,garay_digit
398
+ ๐ต‡,garay_digit
399
+ ๐ตˆ,garay_digit
400
+ ๐ต‰,garay_digit
391
401
  ๐‘ฆ,brahmi_digit
392
402
  ๐‘ง,brahmi_digit
393
403
  ๐‘จ,brahmi_digit
@@ -478,6 +488,26 @@
478
488
  ๐‘›‡,takri_digit
479
489
  ๐‘›ˆ,takri_digit
480
490
  ๐‘›‰,takri_digit
491
+ ๐‘›,myanmar_pao_digit
492
+ ๐‘›‘,myanmar_pao_digit
493
+ ๐‘›’,myanmar_pao_digit
494
+ ๐‘›“,myanmar_pao_digit
495
+ ๐‘›”,myanmar_pao_digit
496
+ ๐‘›•,myanmar_pao_digit
497
+ ๐‘›–,myanmar_pao_digit
498
+ ๐‘›—,myanmar_pao_digit
499
+ ๐‘›˜,myanmar_pao_digit
500
+ ๐‘›™,myanmar_pao_digit
501
+ ๐‘›š,myanmar_eastern_pwo_karen_digit
502
+ ๐‘››,myanmar_eastern_pwo_karen_digit
503
+ ๐‘›œ,myanmar_eastern_pwo_karen_digit
504
+ ๐‘›,myanmar_eastern_pwo_karen_digit
505
+ ๐‘›ž,myanmar_eastern_pwo_karen_digit
506
+ ๐‘›Ÿ,myanmar_eastern_pwo_karen_digit
507
+ ๐‘› ,myanmar_eastern_pwo_karen_digit
508
+ ๐‘›ก,myanmar_eastern_pwo_karen_digit
509
+ ๐‘›ข,myanmar_eastern_pwo_karen_digit
510
+ ๐‘›ฃ,myanmar_eastern_pwo_karen_digit
481
511
  ๐‘œฐ,ahom_digit
482
512
  ๐‘œฑ,ahom_digit
483
513
  ๐‘œฒ,ahom_digit
@@ -508,6 +538,16 @@
508
538
  ๐‘ฅ—,dives_akuru_digit
509
539
  ๐‘ฅ˜,dives_akuru_digit
510
540
  ๐‘ฅ™,dives_akuru_digit
541
+ ๐‘ฏฐ,sunuwar_digit
542
+ ๐‘ฏฑ,sunuwar_digit
543
+ ๐‘ฏฒ,sunuwar_digit
544
+ ๐‘ฏณ,sunuwar_digit
545
+ ๐‘ฏด,sunuwar_digit
546
+ ๐‘ฏต,sunuwar_digit
547
+ ๐‘ฏถ,sunuwar_digit
548
+ ๐‘ฏท,sunuwar_digit
549
+ ๐‘ฏธ,sunuwar_digit
550
+ ๐‘ฏน,sunuwar_digit
511
551
  ๐‘ฑ,bhaiksuki_digit
512
552
  ๐‘ฑ‘,bhaiksuki_digit
513
553
  ๐‘ฑ’,bhaiksuki_digit
@@ -548,6 +588,16 @@
548
588
  ๐‘ฝ—,kawi_digit
549
589
  ๐‘ฝ˜,kawi_digit
550
590
  ๐‘ฝ™,kawi_digit
591
+ ๐–„ฐ,gurung_khema_digit
592
+ ๐–„ฑ,gurung_khema_digit
593
+ ๐–„ฒ,gurung_khema_digit
594
+ ๐–„ณ,gurung_khema_digit
595
+ ๐–„ด,gurung_khema_digit
596
+ ๐–„ต,gurung_khema_digit
597
+ ๐–„ถ,gurung_khema_digit
598
+ ๐–„ท,gurung_khema_digit
599
+ ๐–„ธ,gurung_khema_digit
600
+ ๐–„น,gurung_khema_digit
551
601
  ๐–ฉ ,mro_digit
552
602
  ๐–ฉก,mro_digit
553
603
  ๐–ฉข,mro_digit
@@ -578,6 +628,26 @@
578
628
  ๐–ญ—,pahawh_hmong_digit
579
629
  ๐–ญ˜,pahawh_hmong_digit
580
630
  ๐–ญ™,pahawh_hmong_digit
631
+ ๐–ตฐ,kirat_rai_digit
632
+ ๐–ตฑ,kirat_rai_digit
633
+ ๐–ตฒ,kirat_rai_digit
634
+ ๐–ตณ,kirat_rai_digit
635
+ ๐–ตด,kirat_rai_digit
636
+ ๐–ตต,kirat_rai_digit
637
+ ๐–ตถ,kirat_rai_digit
638
+ ๐–ตท,kirat_rai_digit
639
+ ๐–ตธ,kirat_rai_digit
640
+ ๐–ตน,kirat_rai_digit
641
+ ๐œณฐ,outlined_digit
642
+ ๐œณฑ,outlined_digit
643
+ ๐œณฒ,outlined_digit
644
+ ๐œณณ,outlined_digit
645
+ ๐œณด,outlined_digit
646
+ ๐œณต,outlined_digit
647
+ ๐œณถ,outlined_digit
648
+ ๐œณท,outlined_digit
649
+ ๐œณธ,outlined_digit
650
+ ๐œณน,outlined_digit
581
651
  ๐ŸŽ,mathematical_bold_digit
582
652
  ๐Ÿ,mathematical_bold_digit
583
653
  ๐Ÿ,mathematical_bold_digit
@@ -658,6 +728,16 @@
658
728
  ๐ž“ท,nag_mundari_digit
659
729
  ๐ž“ธ,nag_mundari_digit
660
730
  ๐ž“น,nag_mundari_digit
731
+ ๐ž—ฑ,ol_onal_digit
732
+ ๐ž—ฒ,ol_onal_digit
733
+ ๐ž—ณ,ol_onal_digit
734
+ ๐ž—ด,ol_onal_digit
735
+ ๐ž—ต,ol_onal_digit
736
+ ๐ž—ถ,ol_onal_digit
737
+ ๐ž—ท,ol_onal_digit
738
+ ๐ž—ธ,ol_onal_digit
739
+ ๐ž—น,ol_onal_digit
740
+ ๐ž—บ,ol_onal_digit
661
741
  ๐žฅ,adlam_digit
662
742
  ๐žฅ‘,adlam_digit
663
743
  ๐žฅ’,adlam_digit
@@ -1,15 +1,22 @@
1
1
  # 3. and 4. of Google Chrome IDN policy See https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5B%3AIdentifierStatus%3DAllowed%3A&abb=on&g=&i=
2
2
  class HomographicSpoofing::Detector::Rule::DisallowedCharacters < HomographicSpoofing::Detector::Rule::Base
3
3
  class << self
4
- # See http://kb.mozillazine.org/Network.IDN.blacklist_chars
5
- MOZZILLA_DISALLOWED_CHARACTERS = Set[
4
+ # Characters removed from the allowed set. Combines Mozilla's blocklist
5
+ # (http://kb.mozillazine.org/Network.IDN.blacklist_chars) with the
6
+ # removals in Chromium's IDNSpoofChecker::SetAllowedUnicodeSet
7
+ # (components/url_formatter/spoof_checks/idn_spoof_checker.cc).
8
+ DISALLOWED_CHARACTERS = Set[
6
9
  "\u0020", # Space
7
10
  "\u00a0", # No-break space
8
11
  "\u00bc", # Vulgar fraction one quarter
9
12
  "\u00bd", # Vulgar fraction one half
10
13
  "\u00be", # Vulgar fraction three quarters
14
+ "\u0138", # Latin small letter kra (Chromium)
11
15
  "\u01c3", # Latin letter retroflex click
16
+ "\u02bb", # Modifier letter turned comma (Chromium)
17
+ "\u02bc", # Modifier letter apostrophe (Chromium)
12
18
  "\u02d0", # Modifier letter triangular colon
19
+ "\u02ec", # Modifier letter voicing (Chromium)
13
20
  "\u0337", # Combining short solidus overlay
14
21
  "\u0338", # Combining long solidus overlay
15
22
  "\u0589", # Armenian full stop
@@ -124,11 +131,11 @@ class HomographicSpoofing::Detector::Rule::DisallowedCharacters < HomographicSpo
124
131
  ]
125
132
 
126
133
  def allowed_chars_set
127
- @@allowed_chars_set ||= (read_allowed_idn_chars.chars.to_set - MOZZILLA_DISALLOWED_CHARACTERS)
134
+ @@allowed_chars_set ||= (read_allowed_idn_chars.chars.to_set - DISALLOWED_CHARACTERS)
128
135
  end
129
136
 
130
137
  private
131
- # Built with script/development/generate_allowed_idn_characters.rb
138
+ # Built with bin/generate_allowed_idn_characters
132
139
  def read_allowed_idn_chars
133
140
  File.read("#{__dir__}/data/allowed_idn_characters.txt")
134
141
  end
@@ -1,3 +1,3 @@
1
1
  class HomographicSpoofing::Detector::Rule::Idn::Base < HomographicSpoofing::Detector::Rule::Base
2
- delegate :tld, to: :@context
2
+ delegate :tld, :tld_allowances, to: :@context
3
3
  end
@@ -1,8 +1,14 @@
1
1
  class HomographicSpoofing::Detector::Rule::Idn::Context < HomographicSpoofing::Detector::Rule::Context
2
- attr_reader :tld
2
+ attr_reader :tld, :tld_allowances
3
3
 
4
- def initialize(label:, tld:)
4
+ # `tld_allowances` is shared by every label of one domain, so a decision that
5
+ # depends only on the TLD is made once per domain rather than once per label.
6
+ # Under a wildcard suffix such as "*.mm" the TLD carries a label the sender
7
+ # chose, and scanning it again for every label would cost the label count
8
+ # times its length.
9
+ def initialize(label:, tld:, occurrence: 0, tld_allowances: {})
5
10
  @tld = tld
6
- super(label:)
11
+ @tld_allowances = tld_allowances
12
+ super(label:, occurrence:)
7
13
  end
8
14
  end
@@ -16,6 +16,13 @@ class HomographicSpoofing::Detector::Rule::Idn::DangerousPattern < HomographicSp
16
16
  [\u30ce\u30f3\u30bd\u30be\u4e36\u4e40\u4e41\u4e3f]
17
17
  [^\p{kana}\p{hira}\p{hani}]/x,
18
18
 
19
+ /# Rules see one label at a time, so a label edge counts as a non-Japanese
20
+ # side too: the dot the renderer draws there is not Japanese either. Beside
21
+ # an edge, "ใƒผ" (U+30FC, Common to Unicode's Script property but Japanese in
22
+ # use) counts as Japanese, so words like "ใƒŽใƒผใƒˆ" and "ใ‚ณใƒผใƒณ" stay clear.
23
+ \A[\u30ce\u30f3\u30bd\u30be\u4e36\u4e40\u4e41\u4e3f](?:[^\p{kana}\p{hira}\p{hani}\u30fc]|\z)|
24
+ [^\p{kana}\p{hira}\p{hani}\u30fc][\u30ce\u30f3\u30bd\u30be\u4e36\u4e40\u4e41\u4e3f]\z/x,
25
+
19
26
  /# Disallow three Hiragana letters (U+307[8-A]) or Katakana letters
20
27
  # (U+30D[8-A]) that look exactly like each other when they're used
21
28
  # in a label otherwise entirely in Katakana or Hiragana.
@@ -49,6 +56,14 @@ class HomographicSpoofing::Detector::Rule::Idn::DangerousPattern < HomographicSp
49
56
  [\u4e00\u3127\u4e28\u4e5b\u4e03\u4e05\u5341\u3007\u3112\u311a\u311f\u3128\u3129\u3108\u31ba\u31b3\u5de5\u31b2\u8ba0\u4e01]
50
57
  [^\p{kana}\p{hira}\p{hani}\p{bopo}]/x,
51
58
 
59
+ /# Rules see one label at a time, so a label that is only one of these
60
+ # ideographs has the dots the renderer draws on both sides, and neither
61
+ # is CJK: "a.ไธ€.example.com" reads as "a.-.example.com". A longer label
62
+ # with one at its edge is already caught by the two patterns above when
63
+ # its other neighbour is non-CJK, and stays safe when it is CJK, as in
64
+ # "ๅทฅ่กŒ" or "ไธ€ๅทๅบ—".
65
+ \A[ไธ€ใ„งไธจไน›ไธƒไธ…ๅใ€‡ใ„’ใ„šใ„Ÿใ„จใ„ฉใ„ˆใ†บใ†ณๅทฅใ†ฒ่ฎ ไธ]\z/x,
66
+
52
67
  /# Disallow combining diacritical mark (U+0300-U+0339) after a
53
68
  # non-LGC character. Other combining diacritical marks are not in
54
69
  # the allowed character set.
@@ -9,6 +9,6 @@ class HomographicSpoofing::Detector::Rule::Idn::InvisibleCharacters < Homographi
9
9
  /\u0e48{2,}/, # Thai tone repeated
10
10
  /\u0301{2,}/, # accute accent repeated
11
11
  /\u00e1\u0301/, # 'a' with acuted accent + another acute accent
12
- /^\u0300/, # Combining mark at the beginning
12
+ /\A\p{M}/, # Combining mark at the beginning of a label
13
13
  )
14
14
  end
@@ -46,11 +46,23 @@ class HomographicSpoofing::Detector::Rule::Idn::ScriptConfusable < HomographicSp
46
46
  [ /\p{Mymr}/, /[แ€แ€‚แ€„แ€‘แ€•แ€แ€แ‚แ”แœ\u1090\u1091\u1095\u1096\u1097]/, /[a-z]+\.mm/ ],
47
47
  # # Thai
48
48
  [ /\p{Thai}/, /[เธ—เธ™เธšเธžเธฃเธซเน€เนเนเธ”เธฅเธ›เธŸเธก]/, /th/ ]
49
- ].map { Confusable.new(*_1) }
49
+ ]
50
+ # Anchor each allowed-TLD pattern so it only matches whole trailing labels
51
+ # of the TLD, never a substring of one. Unanchored, `/ru/` matched `.guru`,
52
+ # `/su/` matched `.surf`, `/by/` matched `.baby`/`.rugby`, etc., silently
53
+ # disabling single-script confusable detection on those TLDs. The
54
+ # `(?:\A|\.)โ€ฆ\z` boundary keeps multi-label public suffixes working โ€”
55
+ # `co.th` still matches `/th/`, `com.mm` still matches `/[a-z]+\.mm/`.
56
+ .map do |script, latin_lookalike, allowed_tlds|
57
+ anchored_tlds = allowed_tlds && /(?:\A|\.)(?:#{allowed_tlds.source})\z/
58
+ Confusable.new script, latin_lookalike, anchored_tlds
59
+ end
50
60
 
51
61
  def is_script_confusable_allowed_for_tld?(confusable)
52
- tld_contains_any_letter_from_script?(confusable.script) ||
53
- confusable.allowed_tlds&.match?(tld)
62
+ tld_allowances.fetch(confusable) do
63
+ tld_allowances[confusable] = tld_contains_any_letter_from_script?(confusable.script) ||
64
+ confusable.allowed_tlds&.match?(tld) || false
65
+ end
54
66
  end
55
67
 
56
68
  def tld_contains_any_letter_from_script?(script)
@@ -23,7 +23,7 @@ class HomographicSpoofing::Detector::Rule::MixedDigits < HomographicSpoofing::De
23
23
  end
24
24
  end
25
25
 
26
- # Built with script/development/generate_digits_characters.rb
26
+ # Built with bin/generate_digits_characters
27
27
  def read_digits
28
28
  File.read("#{__dir__}/data/digits.csv")
29
29
  end
@@ -10,19 +10,153 @@ class HomographicSpoofing::Sanitizer::Base
10
10
  end
11
11
 
12
12
  def sanitize
13
- result = field.dup
14
- detector_class.new(field).detections.each do |detection|
15
- log(detection.reason, detection.label)
16
- result = punycode(result, detection.label)
17
- end
18
- result
13
+ detections = detector_class.new(field).detections
14
+ detections.each { |detection| log(detection.reason, detection.label) }
15
+ apply(field, detections)
19
16
  end
20
17
 
21
18
  private
22
19
  attr_reader :field
23
20
 
24
- def punycode(source, label)
25
- source.gsub(label, Dnsruby::Name.punycode(label))
21
+ # Punycode each offending label only within the component it came from. A
22
+ # spanned detection carries the [offset, length] of its component, so a
23
+ # domain-label spoof is confined to the domain and never rewrites a benign
24
+ # local part that merely equals the same string โ€” this is what keeps
25
+ # "ะท@ะผะธั€.ะท.example.com" punycoding the domain label "ะท" while leaving the "ะท"
26
+ # mailbox intact. Spans are disjoint or nested (a display name read from a
27
+ # comment the host's token carries), so they form a tree: each span's text is
28
+ # rebuilt with its inner spans already rewritten, then rewritten itself, in
29
+ # one pass over the field. A detection with no span (a bare IDN or quoted
30
+ # string) spans the whole field and is applied last, over what remains.
31
+ def apply(field, detections)
32
+ return field.dup if detections.empty?
33
+
34
+ whole, spanned = detections.partition { |detection| detection.span.nil? }
35
+ edits = spanned.group_by(&:span).map do |(offset, length), group|
36
+ Edit.new(offset, offset + length, group.map(&:label).uniq, [])
37
+ end
38
+ chars = field.chars
39
+ replace_labels(splice(chars, 0, chars.length, nest(edits)), whole.map(&:label).uniq)
40
+ end
41
+
42
+ Edit = Struct.new(:start, :finish, :labels, :inner)
43
+
44
+ # Arrange spans into a tree, outer before inner, in one pass over them sorted
45
+ # by start. A span that overlaps an earlier sibling without nesting in it is
46
+ # trimmed to start where that sibling ends.
47
+ def nest(edits)
48
+ outermost, open = [], []
49
+ edits.sort_by { |edit| [ edit.start, -edit.finish ] }.each do |edit|
50
+ open.pop while open.any? && open.last.finish < edit.finish
51
+ siblings = open.empty? ? outermost : open.last.inner
52
+ edit.start = [ edit.start, siblings.last.finish ].max if siblings.any?
53
+ next if edit.start > edit.finish
54
+
55
+ siblings << edit
56
+ open << edit
57
+ end
58
+ outermost
59
+ end
60
+
61
+ def splice(chars, start, finish, edits)
62
+ text, at = +"", start
63
+ edits.each do |edit|
64
+ text << chars[at...edit.start].join << replace_labels(splice(chars, edit.start, edit.finish, edit.inner), edit.labels)
65
+ at = edit.finish
66
+ end
67
+ text << chars[at...finish].join
68
+ end
69
+
70
+ # Replace each label where it is a whole "."-delimited component of the
71
+ # region, so an offending label is never rewritten as a substring of a benign
72
+ # sibling (the Cyrillic digit-look-alike "ะท" that also sits inside
73
+ # "ะผะฐะณะฐะทะธะฝ"), and every matching component is punycoded from its own
74
+ # spelling โ€” case-exact, so a spoof repeated in different casing is handled
75
+ # per occurrence. A component is compared with its CFWS (comments and
76
+ # surrounding whitespace, which the parser drops from the label it reports)
77
+ # set aside, and only the label itself is rewritten, so "ะท(comment)" is
78
+ # punycoded at the label and a comment beside it never sends the replacement
79
+ # into a benign sibling. A label that is only a substring of a component (a
80
+ # display name carrying spaces) has no whole component to match and falls
81
+ # back to exact substring replacement.
82
+ #
83
+ # A label that keeps whitespace inside it, as PublicSuffix reports "ะฐ " in
84
+ # "ะ .com", matches its component as written.
85
+ #
86
+ # The region is scanned once for all labels, so sanitizing stays linear in
87
+ # its length however many labels were detected.
88
+ def replace_labels(region, labels)
89
+ return region if labels.empty?
90
+
91
+ components = label_components(region)
92
+ present = components.to_set(&:bare) | components.map(&:text)
93
+ whole, partial = labels.partition { |label| present.include?(label) }
94
+ whole = whole.to_set
95
+
96
+ result = components.map do |component|
97
+ if whole.include?(component.text) && component.bare
98
+ Dnsruby::Name.punycode(component.text)
99
+ elsif whole.include?(component.bare)
100
+ component.punycoded
101
+ else
102
+ component.text
103
+ end
104
+ end.join
105
+ partial.inject(result) { |text, label| text.gsub(label) { Dnsruby::Name.punycode(label) } }
106
+ end
107
+
108
+ # One "."-delimited component of a region: its text, the label it carries
109
+ # with CFWS set aside (`bare`), and where that label starts in the text, past
110
+ # any leading CFWS that may repeat it. Separators are components too, with no
111
+ # label.
112
+ Component = Struct.new(:text, :bare, :start) do
113
+ def punycoded
114
+ if text[start, bare.length] == bare
115
+ text[0, start] + Dnsruby::Name.punycode(bare) + text[(start + bare.length)..]
116
+ else
117
+ text
118
+ end
119
+ end
120
+ end
121
+
122
+ # Split a region into label components on each "." outside comments and
123
+ # quoted strings, in a single pass. Comments nest and honour backslash
124
+ # escapes, as RFC 5322 has them, so a comment's own dots or parentheses
125
+ # never split or end a label early.
126
+ def label_components(region)
127
+ components = []
128
+ text, bare, start = +"", +"", nil
129
+ depth, quoted, escaped = 0, false, false
130
+
131
+ region.each_char do |char|
132
+ commented = depth > 0
133
+ if escaped
134
+ escaped = false
135
+ elsif char == "\\" && (commented || quoted)
136
+ escaped = true
137
+ elsif commented
138
+ depth += 1 if char == "("
139
+ depth -= 1 if char == ")"
140
+ elsif quoted
141
+ quoted = false if char == '"'
142
+ elsif char == "("
143
+ depth, commented = 1, true
144
+ elsif char == '"'
145
+ quoted = true
146
+ elsif char == "."
147
+ components << Component.new(text, bare.strip, start || 0) << Component.new(char, nil, 0)
148
+ text, bare, start = +"", +"", nil
149
+ next
150
+ end
151
+
152
+ unless commented
153
+ start ||= text.length unless char.match?(/\s/)
154
+ bare << char
155
+ end
156
+ text << char
157
+ end
158
+
159
+ components << Component.new(text, bare.strip, start || 0)
26
160
  end
27
161
 
28
162
  def detector_class
@@ -7,4 +7,34 @@ class HomographicSpoofing::Sanitizer::Idn < HomographicSpoofing::Sanitizer::Base
7
7
  def spoofing_type
8
8
  "EmailIDN"
9
9
  end
10
+
11
+ # A bare domain has no comments or quoted strings: the detector splits it on
12
+ # every ".", so split it the same way. Honouring "(" or '"' here would let an
13
+ # unmatched one fold the rest of the domain into one component, and every
14
+ # detected label would then fall back to a substitution over the whole field.
15
+ #
16
+ # The detector reports labels as PublicSuffix does: with the domain's own
17
+ # leading and trailing padding set aside, but any whitespace inside the
18
+ # domain kept on its label ("ะ " in " ะ .ะ .com"). That padding is set apart
19
+ # as components with no label, so each label component reads as its label
20
+ # was reported.
21
+ def label_components(region)
22
+ core = region.strip
23
+ leading = core.empty? ? region : region[0, region.index(core)]
24
+ trailing = region[(leading.length + core.length)..]
25
+
26
+ [ padding(leading), *core.split(/(\.)/, -1).map { |text| component(text) }, padding(trailing) ].compact
27
+ end
28
+
29
+ def component(text)
30
+ if text == "."
31
+ Component.new(text, nil, 0)
32
+ else
33
+ Component.new(text, text.strip, text.index(/\S/) || 0)
34
+ end
35
+ end
36
+
37
+ def padding(text)
38
+ Component.new(text, nil, 0) unless text.empty?
39
+ end
10
40
  end
@@ -1,3 +1,3 @@
1
1
  module HomographicSpoofing
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: homographic_spoofing
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jacopo Beschi
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-06-20 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: zeitwerk
@@ -94,13 +93,27 @@ dependencies:
94
93
  - - ">="
95
94
  - !ruby/object:Gem::Version
96
95
  version: '0'
97
- description:
96
+ - !ruby/object:Gem::Dependency
97
+ name: csv
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '3.3'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '3.3'
98
110
  email:
99
111
  - jacopo@37signals.com
100
112
  executables: []
101
113
  extensions: []
102
114
  extra_rdoc_files: []
103
115
  files:
116
+ - CHANGELOG.md
104
117
  - MIT-LICENSE
105
118
  - README.md
106
119
  - lib/homographic_spoofing.rb
@@ -145,7 +158,9 @@ metadata:
145
158
  allowed_push_host: https://rubygems.org
146
159
  homepage_uri: https://github.com/basecamp/homographic_spoofing
147
160
  source_code_uri: https://github.com/basecamp/homographic_spoofing
148
- post_install_message:
161
+ changelog_uri: https://github.com/basecamp/homographic_spoofing/blob/main/CHANGELOG.md
162
+ bug_tracker_uri: https://github.com/basecamp/homographic_spoofing/issues
163
+ rubygems_mfa_required: 'true'
149
164
  rdoc_options: []
150
165
  require_paths:
151
166
  - lib
@@ -160,8 +175,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
160
175
  - !ruby/object:Gem::Version
161
176
  version: '0'
162
177
  requirements: []
163
- rubygems_version: 3.5.3
164
- signing_key:
178
+ rubygems_version: 3.6.9
165
179
  specification_version: 4
166
180
  summary: A toolkit to both detect and sanitize homographic spoofing attacks in URLs
167
181
  and Email addresses