I watched your video about versioning in CTI Butler.
This feature is useful, but is there a way to get a high-level view of what has changes (vs. specific objects). Something similar to what ATT&CK Sync does?
I watched your video about versioning in CTI Butler.
This feature is useful, but is there a way to get a high-level view of what has changes (vs. specific objects). Something similar to what ATT&CK Sync does?
This doesn’t exist in CTI Butler right now, but is possible if you want to work with the data locally with some of our tools to do this.
Let me explain how;
First, install stix2arango
Then download the ATT&CK data;
curl https://downloads.ctibutler.com/mitre-attack-enterprise-repo-data/enterprise-attack-17_0.json > enterprise-attack-17_0.json
curl https://downloads.ctibutler.com/mitre-attack-enterprise-repo-data/enterprise-attack-18_0.json > enterprise-attack-18_0.json
Now import it;
python3 stix2arango.py \
--file enterprise-attack-17_0.json \
--database demo \
--collection attck \
--stix2arango_note v17 && \
python3 stix2arango.py \
--file enterprise-attack-18_0.json \
--database demo \
--collection attck \
--stix2arango_note v18
The following searches give you the same data in ATT&CK sync.
FOR doc IN attck_vertex_collection
FILTER doc._stix2arango_note IN ["v17", "v18"]
COLLECT id = doc.id, type = doc.type INTO groupedDocs
LET versions = groupedDocs[*].doc
LET createdDates = UNIQUE(versions[*].created)
LET modifiedDates = UNIQUE(versions[*].modified)
LET count_versions = LENGTH(versions)
LET deprecatedInV18 = LENGTH(FOR v IN versions FILTER v._stix2arango_note == "v18" AND v.x_mitre_deprecated == true RETURN v) > 0
LET deprecatedInV17 = LENGTH(FOR v IN versions FILTER v._stix2arango_note == "v17" AND v.x_mitre_deprecated == true RETURN v) > 0
LET revokedInV18 = LENGTH(FOR v IN versions FILTER v._stix2arango_note == "v18" AND v.revoked == true RETURN v) > 0
LET revokedInV17 = LENGTH(FOR v IN versions FILTER v._stix2arango_note == "v17" AND v.revoked == true RETURN v) > 0
// Filter out results where x_mitre_deprecated or revoked is true in both versions
FILTER NOT (deprecatedInV17 == true AND deprecatedInV18 == true)
FILTER NOT (revokedInV17 == true AND revokedInV18 == true)
LET status = (
count_versions == 1 ? "new" :
(revokedInV18 == true AND revokedInV17 == false ? "revoked" :
(deprecatedInV18 == true AND deprecatedInV17 == false ? "deprecated" :
(LENGTH(createdDates) == 1 AND LENGTH(modifiedDates) == 1 ? "unchanged" : "changed")))
)
COLLECT finalStatus = status, finalType = type WITH COUNT INTO count
// Aggregate results by status
COLLECT status = finalStatus INTO typeCounts = {
"type": finalType,
"count": count
}
// Construct dynamic columns for each type
RETURN MERGE(
{ "status": status },
ZIP(
typeCounts[*].type,
typeCounts[*].count
)
)
[
{
"status": "changed",
"x-mitre-tactic": 13,
"x-mitre-data-component": 106,
"tool": 30,
"malware": 262,
"intrusion-set": 58,
"course-of-action": 2,
"campaign": 3,
"x-mitre-matrix": 1,
"attack-pattern": 598
},
{
"status": "deprecated",
"x-mitre-data-source": 37
},
{
"status": "new",
"x-mitre-collection": 1,
"x-mitre-detection-strategy": 691,
"x-mitre-analytic": 1739,
"tool": 1,
"malware": 30,
"intrusion-set": 8,
"course-of-action": 17,
"campaign": 5,
"attack-pattern": 12
},
{
"status": "unchanged",
"x-mitre-tactic": 1,
"tool": 61,
"marking-definition": 1,
"malware": 402,
"intrusion-set": 108,
"identity": 1,
"course-of-action": 42,
"campaign": 44,
"attack-pattern": 81
}
]
FOR doc IN attck_vertex_collection
FILTER doc._stix2arango_note IN ["v17", "v18"]
COLLECT id = doc.id INTO groupedDocs
LET versions = groupedDocs[*].doc
LET count_versions = LENGTH(versions)
// Only include objects that are classified as "new" (present in only one version)
FILTER count_versions == 1
LET v = versions[0]
// Safely handle missing/NULL external_references
LET mitre_attack_id = FIRST(
FOR ref IN (v.external_references || [])
FILTER ref.source_name == "mitre-attack"
RETURN ref.external_id
)
// Optionally skip docs with no mitre-attack ID
FILTER mitre_attack_id != null
RETURN {
type: v.type,
external_id: mitre_attack_id,
name: v.name
}
FOR doc IN attck_vertex_collection
FILTER doc._stix2arango_note IN ["v17", "v18"]
COLLECT id = doc.id INTO groupedDocs
LET versions = groupedDocs[*].doc
LET createdDates = UNIQUE(versions[*].created)
LET modifiedDates = UNIQUE(versions[*].modified)
LET count_versions = LENGTH(versions)
LET deprecatedInV18 = LENGTH(FOR v IN versions FILTER v._stix2arango_note == "v18.0" AND v.x_mitre_deprecated == true RETURN v) > 0
LET deprecatedInV17 = LENGTH(FOR v IN versions FILTER v._stix2arango_note == "v17.0" AND v.x_mitre_deprecated == true RETURN v) > 0
LET revokedInV18 = LENGTH(FOR v IN versions FILTER v._stix2arango_note == "v18.0" AND v.revoked == true RETURN v) > 0
LET revokedInV17 = LENGTH(FOR v IN versions FILTER v._stix2arango_note == "v17.0" AND v.revoked == true RETURN v) > 0
// Filter out results where x_mitre_deprecated or revoked is true in both versions
FILTER NOT (deprecatedInV17 == true AND deprecatedInV18 == true)
FILTER NOT (revokedInV17 == true AND revokedInV18 == true)
// Only include objects that are classified as "changed" (present in both versions with differences in created or modified dates)
LET status = (
count_versions == 1 ? "new" :
(revokedInV18 == true AND revokedInV17 == false ? "revoked" :
(deprecatedInV18 == true AND deprecatedInV17 == false ? "deprecated" :
(LENGTH(createdDates) == 1 AND LENGTH(modifiedDates) == 1 ? "unchanged" : "changed")))
)
FILTER status == "changed"
// Safely extract external_id where source_name is "mitre-attack", handling missing external_references
LET mitre_attack_id = FIRST(
FOR ref IN versions[0].external_references || []
FILTER ref.source_name == "mitre-attack"
RETURN ref.external_id
)
// Return only the specified fields
RETURN {
"type": versions[0].type,
"external_id": mitre_attack_id,
"name": versions[0].name
}
I’ll use T1053.005
LET target_id = "T1053.005"
LET docs = (
FOR doc IN attck_vertex_collection
FILTER doc._stix2arango_note IN ["v17", "v18"] // <-- adjust to what you actually have
AND "mitre-attack" IN (doc.external_references[*].source_name || [])
AND target_id IN (doc.external_references[*].external_id || [])
RETURN { version: doc._stix2arango_note, doc }
)
LET v17_doc = FIRST(FOR d IN docs FILTER d.version == "v17" RETURN d.doc) || null
LET v18_doc = FIRST(FOR d IN docs FILTER d.version == "v18" RETURN d.doc) || null
RETURN (v17_doc != null AND v18_doc != null) ? {
id: target_id,
differences: (
FOR key IN ATTRIBUTES(v17_doc)
FILTER v17_doc[key] != v18_doc[key]
AND !STARTS_WITH(key, "_")
RETURN {
field: key,
v17: v17_doc[key],
v18: v18_doc[key]
}
)
} : {
id: target_id,
error: "One or both versions are missing for comparison."
}