Repository: oasis-open/cti-python-stix2

Issue:
SSDEEP Hashing algorithm case mismatch
Currently if an `ssdeep` hash is added to an object it will change it to its lowercase variant (not aligned with Section 10.6 STIX 2.1 https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_tumklw3o2gyz) because stix2 will "clean" the field name. This causes the validator to emit a warning on a "unrecognized hash".

Validator:
``` 
[!] Warning: file--xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx: {241} Object 'file--xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' has a 'hashes' dictionary with a hash of type 'ssdeep', which is not a value in the hash-algorithm-ov vocabulary nor a custom value prepended with 'x_'.
```

https://github.com/oasis-open/cti-python-stix2/blob/df92770d255d66bd614d4975fc4602c7407998fb/stix2/properties.py#L420

Discussion:

Patch:
diff --git a/stix2/patterns.py b/stix2/patterns.py
index f0cceb8..a44f68e 100644
--- a/stix2/patterns.py
+++ b/stix2/patterns.py
@@ -121,21 +121,21 @@ class BooleanConstant(_Constant):
 
 
 _HASH_REGEX = {
-    "MD5": ("^[a-fA-F0-9]{32}$", "MD5"),
-    "MD6": ("^[a-fA-F0-9]{32}|[a-fA-F0-9]{40}|[a-fA-F0-9]{56}|[a-fA-F0-9]{64}|[a-fA-F0-9]{96}|[a-fA-F0-9]{128}$", "MD6"),
-    "RIPEMD160": ("^[a-fA-F0-9]{40}$", "RIPEMD-160"),
-    "SHA1": ("^[a-fA-F0-9]{40}$", "SHA-1"),
-    "SHA224": ("^[a-fA-F0-9]{56}$", "SHA-224"),
-    "SHA256": ("^[a-fA-F0-9]{64}$", "SHA-256"),
-    "SHA384": ("^[a-fA-F0-9]{96}$", "SHA-384"),
-    "SHA512": ("^[a-fA-F0-9]{128}$", "SHA-512"),
-    "SHA3224": ("^[a-fA-F0-9]{56}$", "SHA3-224"),
-    "SHA3256": ("^[a-fA-F0-9]{64}$", "SHA3-256"),
-    "SHA3384": ("^[a-fA-F0-9]{96}$", "SHA3-384"),
-    "SHA3512": ("^[a-fA-F0-9]{128}$", "SHA3-512"),
-    "SSDEEP": ("^[a-zA-Z0-9/+:.]{1,128}$", "ssdeep"),
-    "WHIRLPOOL": ("^[a-fA-F0-9]{128}$", "WHIRLPOOL"),
-    "TLSH": ("^[a-fA-F0-9]{70}$", "TLSH"),
+    "MD5": (r"^[a-fA-F0-9]{32}$", "MD5"),
+    "MD6": (r"^[a-fA-F0-9]{32}|[a-fA-F0-9]{40}|[a-fA-F0-9]{56}|[a-fA-F0-9]{64}|[a-fA-F0-9]{96}|[a-fA-F0-9]{128}$", "MD6"),
+    "RIPEMD160": (r"^[a-fA-F0-9]{40}$", "RIPEMD-160"),
+    "SHA1": (r"^[a-fA-F0-9]{40}$", "SHA-1"),
+    "SHA224": (r"^[a-fA-F0-9]{56}$", "SHA-224"),
+    "SHA256": (r"^[a-fA-F0-9]{64}$", "SHA-256"),
+    "SHA384": (r"^[a-fA-F0-9]{96}$", "SHA-384"),
+    "SHA512": (r"^[a-fA-F0-9]{128}$", "SHA-512"),
+    "SHA3224": (r"^[a-fA-F0-9]{56}$", "SHA3-224"),
+    "SHA3256": (r"^[a-fA-F0-9]{64}$", "SHA3-256"),
+    "SHA3384": (r"^[a-fA-F0-9]{96}$", "SHA3-384"),
+    "SHA3512": (r"^[a-fA-F0-9]{128}$", "SHA3-512"),
+    "SSDEEP": (r"^[a-zA-Z0-9/+:.]{1,128}$", "SSDEEP"),
+    "WHIRLPOOL": (r"^[a-fA-F0-9]{128}$", "WHIRLPOOL"),
+    "TLSH": (r"^[a-fA-F0-9]{70}$", "TLSH"),
 }
 
 
diff --git a/stix2/properties.py b/stix2/properties.py
index a1bab6d..c876c11 100644
--- a/stix2/properties.py
+++ b/stix2/properties.py
@@ -417,7 +417,7 @@ HASHES_REGEX = {
     "SHA3256": (r"^[a-fA-F0-9]{64}$", "SHA3-256"),
     "SHA3384": (r"^[a-fA-F0-9]{96}$", "SHA3-384"),
     "SHA3512": (r"^[a-fA-F0-9]{128}$", "SHA3-512"),
-    "SSDEEP": (r"^[a-zA-Z0-9/+:.]{1,128}$", "ssdeep"),
+    "SSDEEP": (r"^[a-zA-Z0-9/+:.]{1,128}$", "SSDEEP"),
     "WHIRLPOOL": (r"^[a-fA-F0-9]{128}$", "WHIRLPOOL"),
     "TLSH": (r"^[a-fA-F0-9]{70}$", "TLSH"),
 }
@@ -431,6 +431,8 @@ class HashesProperty(DictionaryProperty):
             key = k.upper().replace('-', '')
             if key in HASHES_REGEX:
                 vocab_key = HASHES_REGEX[key][1]
+                if vocab_key == "SSDEEP" and self.spec_version == "2.0":
+                    vocab_key = vocab_key.lower()
                 if not re.match(HASHES_REGEX[key][0], v):
                     raise ValueError("'{0}' is not a valid {1} hash".format(v, vocab_key))
                 if k != vocab_key:


Test patch:
diff --git a/stix2/test/v20/test_observed_data.py b/stix2/test/v20/test_observed_data.py
index bfe9c34..354d70c 100644
--- a/stix2/test/v20/test_observed_data.py
+++ b/stix2/test/v20/test_observed_data.py
@@ -714,6 +714,22 @@ def test_file_example():
     assert f.decryption_key == "fred"   # does the key have a format we can test for?
 
 
+def test_file_ssdeep_example():
+    f = stix2.v20.File(
+        name="example.dll",
+        hashes={
+            "SHA-256": "ceafbfd424be2ca4a5f0402cae090dda2fb0526cf521b60b60077c0f622b285a",
+            "ssdeep": "96:gS/mFkCpXTWLr/PbKQHbr/S/mFkCpXTWLr/PbKQHbrB:Tu6SXTWGQHbeu6SXTWGQHbV",
+        },
+        size=1024,
+    )
+
+    assert f.name == "example.dll"
+    assert f.size == 1024
+    assert f.hashes["SHA-256"] == "ceafbfd424be2ca4a5f0402cae090dda2fb0526cf521b60b60077c0f622b285a"
+    assert f.hashes["ssdeep"] == "96:gS/mFkCpXTWLr/PbKQHbr/S/mFkCpXTWLr/PbKQHbrB:Tu6SXTWGQHbeu6SXTWGQHbV"
+
+
 def test_file_example_with_NTFSExt():
     f = stix2.v20.File(
         name="abc.txt",
diff --git a/stix2/test/v21/test_observed_data.py b/stix2/test/v21/test_observed_data.py
index b8732be..c13148a 100644
--- a/stix2/test/v21/test_observed_data.py
+++ b/stix2/test/v21/test_observed_data.py
@@ -785,6 +785,22 @@ def test_file_example():
     assert f.atime == dt.datetime(2016, 12, 21, 20, 0, 0, tzinfo=pytz.utc)
 
 
+def test_file_ssdeep_example():
+    f = stix2.v21.File(
+        name="example.dll",
+        hashes={
+            "SHA-256": "ceafbfd424be2ca4a5f0402cae090dda2fb0526cf521b60b60077c0f622b285a",
+            "SSDEEP": "96:gS/mFkCpXTWLr/PbKQHbr/S/mFkCpXTWLr/PbKQHbrB:Tu6SXTWGQHbeu6SXTWGQHbV",
+        },
+        size=1024,
+    )
+
+    assert f.name == "example.dll"
+    assert f.size == 1024
+    assert f.hashes["SHA-256"] == "ceafbfd424be2ca4a5f0402cae090dda2fb0526cf521b60b60077c0f622b285a"
+    assert f.hashes["SSDEEP"] == "96:gS/mFkCpXTWLr/PbKQHbr/S/mFkCpXTWLr/PbKQHbrB:Tu6SXTWGQHbeu6SXTWGQHbV"
+
+
 def test_file_example_with_NTFSExt():
     f = stix2.v21.File(
         name="abc.txt",
diff --git a/stix2/test/v21/test_pattern_expressions.py b/stix2/test/v21/test_pattern_expressions.py
index 198edac..8294a41 100644
--- a/stix2/test/v21/test_pattern_expressions.py
+++ b/stix2/test/v21/test_pattern_expressions.py
@@ -518,7 +518,7 @@ def test_invalid_boolean_constant():
 @pytest.mark.parametrize(
     "hashtype, data", [
         ('MD5', 'zzz'),
-        ('ssdeep', 'zzz=='),
+        ('SSDEEP', 'zzz=='),
     ],
 )
 def test_invalid_hash_constant(hashtype, data):