Skip to content

Commit bcb3c53

Browse files
committed
Merge branch 'develop' into detections-updates
2 parents 4079a75 + c6b1b7a commit bcb3c53

File tree

7 files changed

+187
-90
lines changed

7 files changed

+187
-90
lines changed

.github/workflows/gh-pages.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
INCLUDE_OSANO: true
5151

5252
- name: Cleanup build
53-
run: rm -rf attack-versions
53+
run: rm -rf attack-version-archives
5454

5555
- name: Remove STIX directory
5656
run: rm -rf output/stix/

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
# v4.3.0 (2025-09-25)
2+
3+
## Features
4+
5+
* Update versions module to speed up how previous versions are created
6+
* Previous versions of website only archive ATT&CK object pages and redirect Resource pages to the latest version
7+
* Add Excel file table to ATT&CK data and tools for easier access to previous versions
8+
19
# v4.2.3 (2025-05-06)
210

311
## Features

modules/resources/templates/attack-data-and-tools.html

Lines changed: 97 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -320,21 +320,76 @@ <h3 class="mt-5">ATT&amp;CK in Excel</h3>
320320
<h5 class="fw-bold mb-3" style="font-size:1.1rem;">ATT&CK Excel Files by Version</h5>
321321
<ul class="nav nav-tabs mb-2" role="tablist">
322322
{% set versions = parsed.excel_files_by_version.keys()|list %}
323-
{% for version in versions %}
324-
{% if loop.first %}
325-
<li class="nav-item">
326-
<a class="nav-link active" id="tab-{{ version|replace('.', '-') }}" data-toggle="tab" href="#pane-{{ version|replace('.', '-') }}" role="tab" aria-controls="pane-{{ version|replace('.', '-') }}" aria-selected="true">
327-
{{ version }}
328-
</a>
329-
</li>
330-
{% else %}
331-
<li class="nav-item">
332-
<a class="nav-link" id="tab-{{ version|replace('.', '-') }}" data-toggle="tab" href="#pane-{{ version|replace('.', '-') }}" role="tab" aria-controls="pane-{{ version|replace('.', '-') }}" aria-selected="false">
333-
{{ version }}
334-
</a>
335-
</li>
336-
{% endif %}
337-
{% endfor %}
323+
{% set visible_count = 3 %}
324+
{% if versions|length <= visible_count %}
325+
{% for version in versions %}
326+
{% if loop.first %}
327+
<li class="nav-item">
328+
<a class="nav-link active" id="tab-{{ version|replace('.', '-') }}" data-toggle="tab" href="#pane-{{ version|replace('.', '-') }}" role="tab" aria-selected="true">
329+
{{ version }}
330+
</a>
331+
</li>
332+
{% else %}
333+
<li class="nav-item">
334+
<a class="nav-link" id="tab-{{ version|replace('.', '-') }}" data-toggle="tab" href="#pane-{{ version|replace('.', '-') }}" role="tab" aria-selected="false">
335+
{{ version }}
336+
</a>
337+
</li>
338+
{% endif %}
339+
{% endfor %}
340+
{% else %}
341+
{% for version in versions[:visible_count] %}
342+
{% if loop.first %}
343+
<li class="nav-item">
344+
<a class="nav-link active" id="tab-{{ version|replace('.', '-') }}" data-toggle="tab" href="#pane-{{ version|replace('.', '-') }}" role="tab" aria-selected="true">
345+
{{ version }}
346+
</a>
347+
</li>
348+
{% else %}
349+
<li class="nav-item">
350+
<a class="nav-link" id="tab-{{ version|replace('.', '-') }}" data-toggle="tab" href="#pane-{{ version|replace('.', '-') }}" role="tab" aria-selected="false">
351+
{{ version }}
352+
</a>
353+
</li>
354+
{% endif %}
355+
{% endfor %}
356+
<li class="nav-item dropdown">
357+
<!-- Localized override for the "Older versions" dropdown so it looks like a page-level dropdown
358+
rather than inheriting the top navigation (.nav/.navbar) styles in style-attack.css.
359+
We intentionally scope the CSS here (template-only change) and add an extra class on
360+
the dropdown menu. -->
361+
<style>
362+
/* Scoped to this dropdown; uses !important to override global navbar rules */
363+
.older-versions-dropdown {
364+
background-color: white !important;
365+
box-shadow: 0 8px 16px rgba(0,0,0,0.08);
366+
}
367+
.older-versions-dropdown .dropdown-item {
368+
color: #39434c !important;
369+
}
370+
.older-versions-dropdown .dropdown-item.active {
371+
background-color: white !important;
372+
}
373+
.older-versions-dropdown .dropdown-item:hover,
374+
.older-versions-dropdown .dropdown-item:focus {
375+
color: #4f7cac !important;
376+
}
377+
</style>
378+
379+
<a class="nav-link dropdown-toggle" id="older-versions-dropdown" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
380+
Older versions
381+
</a>
382+
<ul class="dropdown-menu older-versions-dropdown" aria-labelledby="older-versions-dropdown">
383+
{% for version in versions[visible_count:] %}
384+
<li>
385+
<a class="dropdown-item" id="tab-{{ version|replace('.', '-') }}" data-toggle="tab" href="#pane-{{ version|replace('.', '-') }}" role="tab" aria-selected="false">
386+
{{ version }}
387+
</a>
388+
</li>
389+
{% endfor %}
390+
</ul>
391+
</li>
392+
{% endif %}
338393
</ul>
339394
<div class="tab-content card card-body p-3" style="background: #f8f9fa;">
340395
{% for version in versions %}
@@ -371,5 +426,32 @@ <h5 class="fw-bold mb-3" style="font-size:1.1rem;">ATT&CK Excel Files by Version
371426
{% block scripts %}
372427
{{ super() }}
373428
<!--SCRIPTS-->
429+
430+
<!-- Script to manage active states for dropdown tabs -->
431+
<script>
432+
$(document).ready(function() {
433+
// Handle tab switching for all tabs (including dropdown items)
434+
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
435+
var activeTab = $(e.target);
436+
var previousTab = $(e.relatedTarget);
437+
438+
// Remove active class from all dropdown items
439+
$('.dropdown-item[data-toggle="tab"]').removeClass('active');
440+
441+
// If the newly active tab is a dropdown item, add active class
442+
if (activeTab.hasClass('dropdown-item')) {
443+
activeTab.addClass('active');
444+
}
445+
446+
});
447+
448+
// Optional: Close dropdown when an item is clicked
449+
$('.dropdown-item[data-toggle="tab"]').on('click', function() {
450+
$(this).closest('.dropdown').removeClass('show');
451+
$(this).closest('.dropdown-menu').removeClass('show');
452+
});
453+
});
454+
</script>
455+
374456
<script src="/theme/scripts/sidebar-load-all.js"></script>
375457
{% endblock %}

modules/techniques/techniques.py

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,17 @@ def generate_domain_markdown(domain, techniques_no_sub, tactics, side_nav_data,
8888
md_file.write(subs)
8989

9090
# Create the markdown for techniques in the STIX
91+
datasource_of = util.relationshipgetters.get_datasource_of()
9192
for technique in techniques_no_sub[domain]:
9293
if "revoked" not in technique or technique["revoked"] is False:
93-
generate_technique_md(technique, domain, side_nav_data, tactics[domain], notes)
94+
generate_technique_md(technique, domain, side_nav_data, tactics[domain], notes, datasource_of)
9495

9596
return True
9697

9798
return False
9899

99100

100-
def generate_technique_md(technique, domain, side_nav_data, tactic_list, notes):
101+
def generate_technique_md(technique, domain, side_nav_data, tactic_list, notes, datasource_of):
101102
"""Generetes markdown data for given technique."""
102103
attack_id = util.buildhelpers.get_attack_id(technique)
103104

@@ -117,7 +118,7 @@ def generate_technique_md(technique, domain, side_nav_data, tactic_list, notes):
117118
technique_dict["subtechniques"] = get_subtechniques(technique)
118119

119120
# Generate data for technique
120-
technique_dict = generate_data_for_md(technique_dict, technique, tactic_list)
121+
technique_dict = generate_data_for_md(technique_dict=technique_dict, technique=technique, tactic_list=tactic_list, datasource_of=datasource_of)
121122

122123
subs = techniques_config.technique_md.substitute(technique_dict)
123124
path = technique_dict["attack_id"]
@@ -143,7 +144,7 @@ def generate_technique_md(technique, domain, side_nav_data, tactic_list, notes):
143144
sub_tech_dict["parent_name"] = technique.get("name")
144145
sub_tech_dict["subtechniques"] = technique_dict["subtechniques"]
145146

146-
sub_tech_dict = generate_data_for_md(sub_tech_dict, subtechnique["object"], tactic_list, True)
147+
sub_tech_dict = generate_data_for_md(technique_dict=sub_tech_dict, technique=subtechnique["object"], tactic_list=tactic_list, is_sub_technique=True, datasource_of=datasource_of)
147148

148149
if sub_tech_dict.get("sub_number"):
149150
subs = techniques_config.sub_technique_md.substitute(sub_tech_dict)
@@ -158,7 +159,7 @@ def generate_technique_md(technique, domain, side_nav_data, tactic_list, notes):
158159
md_file.write(subs)
159160

160161

161-
def generate_data_for_md(technique_dict, technique, tactic_list, is_sub_technique=False):
162+
def generate_data_for_md(technique_dict, technique, tactic_list, is_sub_technique=False, datasource_of=None):
162163
"""Given a technique or subtechnique, fill technique dictionary to create markdown file."""
163164
technique_dict["name"] = technique.get("name")
164165

@@ -266,10 +267,10 @@ def generate_data_for_md(technique_dict, technique, tactic_list, is_sub_techniqu
266267
technique_dict["eff_perms"] = ", ".join(technique["x_mitre_effective_permissions"])
267268

268269
# Get data sources and components
269-
# (
270-
# technique_dict["datasources"],
271-
# technique_dict["show_descriptions"],
272-
# ) = get_datasources_and_components_of_technique(technique, reference_list)
270+
(
271+
technique_dict["datasources"],
272+
technique_dict["show_descriptions"],
273+
) = get_datasources_and_components_of_technique(technique, reference_list, datasource_of)
273274

274275
# Get if technique supports remote
275276
if technique.get("x_mitre_remote_support"):
@@ -341,9 +342,11 @@ def generate_data_for_md(technique_dict, technique, tactic_list, is_sub_techniqu
341342

342343

343344
def get_mitigations_table_data(technique, reference_list):
344-
"""Given a technique a reference list, find mitigations that mitigate
345-
technique and return list with mitigation data. Also modifies the
346-
reference list if it finds a reference that is not on the list
345+
"""Given a technique and a reference list, return mitigation data.
346+
347+
Find mitigations that mitigate the technique and return a list with mitigation
348+
data. Also modifies the reference list if it finds a reference that is not
349+
on the list.
347350
"""
348351
mitigation_data = []
349352

@@ -373,9 +376,11 @@ def get_mitigations_table_data(technique, reference_list):
373376

374377

375378
def get_assets_table_data(technique, reference_list):
376-
"""Given a technique a reference list, find assets that are targeted by the
377-
technique and return list with asset data. Also modifies the
378-
reference list if it finds a reference that is not on the list
379+
"""Return asset data for a technique.
380+
381+
Given a technique and a reference list, find assets targeted by the
382+
technique and return a list with asset data. Also modify the reference
383+
list if a relationship reference is not present in the list.
379384
"""
380385
asset_data = []
381386

@@ -407,9 +412,10 @@ def get_assets_table_data(technique, reference_list):
407412

408413

409414
def get_examples_table_data(technique, reference_list):
410-
"""Given a technique object, find examples in malware using technique,
411-
tools using technique and groups using technique. Return list with
412-
example data
415+
"""Return example data for a technique.
416+
417+
Find malware, tools, groups, and campaigns that use the technique and
418+
return a list with example data.
413419
"""
414420
# Creating map to avoid repeating the code multiple times
415421
examples_map = [
@@ -451,7 +457,7 @@ def get_examples_table_data(technique, reference_list):
451457

452458

453459
def get_path_from_type(object):
454-
"""Given an object, return the path"""
460+
"""Return the path for an object."""
455461
path_map = {"intrusion-set": "groups", "malware": "software", "tool": "software", "campaign": "campaigns"}
456462
return path_map[object.get("type")]
457463

@@ -528,7 +534,7 @@ def get_technique_side_nav_data(techniques, tactics):
528534

529535

530536
def get_techniques_list(techniques):
531-
"""This method is used to generate a list of techniques."""
537+
"""Generate a list of techniques."""
532538
technique_list = {}
533539

534540
for technique in techniques:
@@ -584,9 +590,10 @@ def get_subtechniques(technique):
584590
return sorted(subtechs, key=lambda k: k["id"])
585591

586592

587-
def get_datasources_and_components_of_technique(technique, reference_list):
588-
"""Given a technique object, find data sources and components
589-
detecting the technique. Returns list with the following structure
593+
def get_datasources_and_components_of_technique(technique, reference_list, datasource_of):
594+
"""Return data sources and components that detect a technique.
595+
596+
Returns a list with the following structure:
590597
591598
For each data source:
592599
Data Source ATT&CK ID
@@ -598,20 +605,22 @@ def get_datasources_and_components_of_technique(technique, reference_list):
598605
datasource_and_components = []
599606

600607
datacomponents_of_technique = util.relationshipgetters.get_datacomponents_detecting_technique().get(technique["id"])
601-
datasource_of = util.relationshipgetters.get_datasource_of()
602608

603609
show_descriptions = False
604610

605611
if datacomponents_of_technique:
606612
datasources_data = {}
607613
for datacomponent in datacomponents_of_technique:
608614
datasource = datasource_of.get(datacomponent["object"]["id"])
615+
# If datasource lookup failed, skip this datacomponent
616+
if not datasource:
617+
continue
609618
datasource_attack_id = util.buildhelpers.get_attack_id(datasource)
610619
if datasource_attack_id:
611620
if not datasources_data.get(datasource_attack_id):
612621
datasources_data[datasource_attack_id] = {}
613622
datasources_data[datasource_attack_id]["attack_id"] = datasource_attack_id
614-
datasources_data[datasource_attack_id]["name"] = datasource["name"]
623+
datasources_data[datasource_attack_id]["name"] = datasource.get("name")
615624
datasources_data[datasource_attack_id]["datacomponents"] = []
616625

617626
datacomponent_data = {}

0 commit comments

Comments
 (0)