From 1be2e57e522936a92d79c8d5628f7e03100cd0c8 Mon Sep 17 00:00:00 2001 From: Hare Sudhan Date: Tue, 25 Nov 2025 23:32:49 -0500 Subject: [PATCH] python conversion --- atomic_red_team/atomic_doc_template.md.erb | 99 -- atomic_red_team/atomic_doc_template.md.j2 | 85 ++ .../atomic_execution_template.html.erb | 1 - atomic_red_team/atomic_red_team.rb | 271 ----- atomic_red_team/attack_api.py | 238 +++++ atomic_red_team/attack_api.rb | 119 --- atomic_red_team/doc_generator.py | 729 ++++++++++++++ atomic_red_team/models.py | 19 +- atomic_red_team/runner.py | 68 +- atomic_red_team/utils.py | 374 +++++++ atomics/T1562.004/T1562.004.md | 4 - atomics/T1562.004/T1562.004.yaml | 1 - poetry.lock | 926 +++++++++++++++++- pyproject.toml | 2 + 14 files changed, 2435 insertions(+), 501 deletions(-) delete mode 100644 atomic_red_team/atomic_doc_template.md.erb create mode 100644 atomic_red_team/atomic_doc_template.md.j2 delete mode 100644 atomic_red_team/atomic_execution_template.html.erb delete mode 100755 atomic_red_team/atomic_red_team.rb create mode 100644 atomic_red_team/attack_api.py delete mode 100755 atomic_red_team/attack_api.rb create mode 100644 atomic_red_team/doc_generator.py create mode 100644 atomic_red_team/utils.py diff --git a/atomic_red_team/atomic_doc_template.md.erb b/atomic_red_team/atomic_doc_template.md.erb deleted file mode 100644 index 3cb8d7ee..00000000 --- a/atomic_red_team/atomic_doc_template.md.erb +++ /dev/null @@ -1,99 +0,0 @@ -# <%= technique['identifier'] %> - <%= technique['name'] -%> - -## [Description from ATT&CK](https://attack.mitre.org/techniques/<%= technique['identifier'].gsub(/\./, '/') %>) -
- -<%= technique['description'].gsub("%\\<", "%<") %> - -
- -## Atomic Tests -<% atomic_yaml['atomic_tests'].each_with_index do |test, test_number| -%> -<% title = "Atomic Test ##{test_number+1} - #{test['name']}" %> -- [<%= title %>](#<%= title.downcase.gsub(/ /, '-').gsub(/[`~!@#$%^&*()+=<>?,.\/:;"'|{}\[\]\\–—]/, '') %>) -<% end %> - -<% atomic_yaml['atomic_tests'].each_with_index do |test, test_number| -%> -
- -## Atomic Test #<%= test_number+1 %> - <%= test['name'] %> -<%= test['description'].strip -%> - - -**Supported Platforms:** <%= test['supported_platforms'].collect do |p| - case p - when 'macos' - 'macOS' - else - p.capitalize - end -end.join(', ') %> - - -**auto_generated_guid:** <%= test['auto_generated_guid'] %> - - -<%def cleanup(input) - input.to_s.strip.gsub(/\\/,"\") -end%> - -<% if test['input_arguments'].to_a.count > 0 %> -#### Inputs: -| Name | Description | Type | Default Value | -|------|-------------|------|---------------| -<% test['input_arguments'].each do |arg_name, arg_options| -%> -| <%= cleanup(arg_name) %> | <%= cleanup(arg_options['description']) %> | <%= cleanup(arg_options['type']) %> | <%= cleanup(arg_options['default']) %>| -<% end -%> -<% end -%> - -<%- if test['executor']['name'] == 'manual' -%> -#### Run it with these steps! <%- if test['executor']['elevation_required'] -%> Elevation Required (e.g. root or admin) <%- end -%> - -<%= test['executor']['steps'] %> -<%- else -%> - -#### Attack Commands: Run with `<%= test['executor']['name'] %>`! <%- if test['executor']['elevation_required'] -%> Elevation Required (e.g. root or admin) <%- end -%> - -<%def get_language(executor) - language = executor - if executor == "command_prompt" - language = "cmd" - elsif executor == "manual" - language = "" - end - language -end%> - -```<%= get_language(test['executor']['name']) %> -<%= test['executor']['command'].to_s.strip %> -``` -<%- end -%> - -<%- if test['executor']['cleanup_command'] != nil -%> -#### Cleanup Commands: -```<%= get_language(test['executor']['name']) %> -<%= test['executor']['cleanup_command'].to_s.strip %> -``` -<%- end -%> - -<% if test['dependencies'].to_a.count > 0 %> -<% dependency_executor = test['executor']['name'] %> -#### Dependencies: Run with `<%- if test['dependency_executor_name'] != nil%><% dependency_executor = test['dependency_executor_name'] %><%= test['dependency_executor_name'] %><%- else -%><%= test['executor']['name'] %><%- end -%>`! -<% test['dependencies'].each do | dep | -%> -##### Description: <%= dep['description'].strip %> -##### Check Prereq Commands: -```<%= get_language(dependency_executor) %> -<%= dep['prereq_command'].strip %> -``` -##### Get Prereq Commands: -```<%= get_language(dependency_executor) %> -<%= dep['get_prereq_command'].strip %> -``` -<% end -%> -<% end -%> - - - - -
-<%- end -%> diff --git a/atomic_red_team/atomic_doc_template.md.j2 b/atomic_red_team/atomic_doc_template.md.j2 new file mode 100644 index 00000000..e144bf29 --- /dev/null +++ b/atomic_red_team/atomic_doc_template.md.j2 @@ -0,0 +1,85 @@ +# {{ technique['identifier'] }} - {{ technique['name'] }} +## [Description from ATT&CK](https://attack.mitre.org/techniques/{{ technique['identifier'].replace('.', '/') }}) +
+ +{{ technique['description'].replace("%\\<", "%<") }} + +
+ +## Atomic Tests + +{% for test in atomic_yaml['atomic_tests'] -%} +{% set title = "Atomic Test #" ~ (loop.index) ~ " - " ~ test['name'] -%} +- [{{ title }}](#{{ title | slugify }}) + +{% endfor %} + +{% for test in atomic_yaml['atomic_tests'] -%} +
+ +## Atomic Test #{{ loop.index }} - {{ test['name'] }} +{{ test['description'].strip() }} + +**Supported Platforms:** {{ test['supported_platforms'] | map('platform_display') | join(', ') }} + + +**auto_generated_guid:** {{ test['auto_generated_guid'] }} + + + + + +{% if test.get('input_arguments') and test['input_arguments'] | length > 0 %} +#### Inputs: +| Name | Description | Type | Default Value | +|------|-------------|------|---------------| +{% for arg_name, arg_options in test['input_arguments'].items() -%} +| {{ arg_name | cleanup }} | {{ arg_options['description'] | cleanup }} | {{ arg_options['type'] | cleanup }} | {{ arg_options.get('default', '') | cleanup }}| +{% endfor %} + +{% endif %} +{%- if test['executor']['name'] == 'manual' %} +#### Run it with these steps! {% if test['executor'].get('elevation_required') %} Elevation Required (e.g. root or admin) {% endif %} + +{{ test['executor']['steps'] }} + +{% else %} + +#### Attack Commands: Run with `{{ test['executor']['name'] }}`! {% if test['executor'].get('elevation_required') %} Elevation Required (e.g. root or admin) {% endif %} + + + +```{{ test['executor']['name'] | get_language }} +{{ test['executor']['command'].strip() }} +``` + +{% if test['executor'].get('cleanup_command') %} +#### Cleanup Commands: +```{{ test['executor']['name'] | get_language }} +{{ test['executor']['cleanup_command'].strip() }} +``` +{% endif %} +{% endif %} + + + +{% if test.get('dependencies') and test['dependencies'] | length > 0 -%} +#### Dependencies: Run with `{{ test.get('dependency_executor_name') or test['executor']['name'] }}`! +{% for dep in test['dependencies'] -%} +##### Description: {{ dep['description'].strip() }} +##### Check Prereq Commands: +```{{ (test.get('dependency_executor_name') or test['executor']['name']) | get_language }} +{{ dep['prereq_command'].strip() }} +``` +##### Get Prereq Commands: +```{{ (test.get('dependency_executor_name') or test['executor']['name']) | get_language }} +{{ dep['get_prereq_command'].strip() }} +``` +{% endfor %} + + +{% endif %} + + +
+{% endfor -%} diff --git a/atomic_red_team/atomic_execution_template.html.erb b/atomic_red_team/atomic_execution_template.html.erb deleted file mode 100644 index 2fd9f957..00000000 --- a/atomic_red_team/atomic_execution_template.html.erb +++ /dev/null @@ -1 +0,0 @@ -TBD \ No newline at end of file diff --git a/atomic_red_team/atomic_red_team.rb b/atomic_red_team/atomic_red_team.rb deleted file mode 100755 index 2e158a58..00000000 --- a/atomic_red_team/atomic_red_team.rb +++ /dev/null @@ -1,271 +0,0 @@ -require 'yaml' -require 'erb' -require 'attack_api' -require 'securerandom' - -class AtomicRedTeam - ATTACK_API = Attack.new - - ATOMICS_DIRECTORY = "#{File.dirname(File.dirname(__FILE__))}/atomics" - - # TODO- should these all be relative URLs? - ROOT_GITHUB_URL = "https://github.com/redcanaryco/atomic-red-team" - - # - # Returns a list of paths that contain Atomic Tests - # - def atomic_test_paths - Dir["#{ATOMICS_DIRECTORY}/T*/T*.yaml"].sort - end - - # - # Returns a list of Atomic Tests in Atomic Red Team (as Hashes from source YAML) - # - def atomic_tests - @atomic_tests ||= atomic_test_paths.collect do |path| - atomic_yaml = YAML.load(File.read path) - atomic_yaml['atomic_yaml_path'] = path - atomic_yaml - end - end - - # - # Returns the individual Atomic Tests for a given identifer, passed as either a string (T1234) or an ATT&CK technique object - # - def atomic_tests_for_technique_by_platform(technique_or_technique_identifier, platform) - technique_identifier = if technique_or_technique_identifier.is_a? Hash - ATTACK_API.technique_identifier_for_technique technique_or_technique_identifier - else - technique_or_technique_identifier - end - - test_list = Array.new - atomic_tests.find do |atomic_yaml| - if atomic_yaml.fetch('attack_technique').upcase == technique_identifier.upcase - atomic_yaml['atomic_tests'].each do |a_test| - if a_test["supported_platforms"].include?(platform[:platform]) - test_list.append(a_test) - end - end - end - end - test_list - end - - # - # Returns the individual Atomic Tests for a given identifer, passed as either a string (T1234) or an ATT&CK technique object - # - def atomic_tests_for_technique(technique_or_technique_identifier) - technique_identifier = if technique_or_technique_identifier.is_a? Hash - ATTACK_API.technique_identifier_for_technique technique_or_technique_identifier - else - technique_or_technique_identifier - end - - atomic_tests.find do |atomic_yaml| - atomic_yaml.fetch('attack_technique').upcase == technique_identifier.upcase - end.to_h.fetch('atomic_tests', []) - end - - # - # Returns a Markdown formatted Github link to a technique. This will be to the edit page for - # techniques that already have one or more Atomic Red Team tests, or the create page for - # techniques that have no existing tests for the given OS. - # - def github_link_to_technique(technique, include_identifier: false, only_platform: self.only_platform) - technique_identifier = ATTACK_API.technique_identifier_for_technique(technique).upcase - link_display = "#{"#{technique_identifier.upcase} " if include_identifier}#{technique['name']}" - yaml_file = "#{ATOMICS_DIRECTORY}/#{technique_identifier}/#{technique_identifier}.yaml" - markdown_file = "#{ATOMICS_DIRECTORY}/#{technique_identifier}/#{technique_identifier}.md" - - if atomic_yaml_has_test_for_platform(yaml_file, only_platform) && (File.exist? markdown_file) - # we have a file for this technique, so link to it's Markdown file - "[#{link_display}](../../#{technique_identifier}/#{technique_identifier}.md)" - else - # we don't have a file for this technique, or there are not tests for the given platform, so link to an edit page - "#{link_display} [CONTRIBUTE A TEST](https://github.com/redcanaryco/atomic-red-team/wiki/Contributing)" - end - end - - def atomic_yaml_has_test_for_platform(yaml_file, only_platform) - has_test_for_platform = false - if File.exist? yaml_file - yaml = YAML.load_file(yaml_file) - yaml['atomic_tests'].each_with_index do |atomic, i| - if atomic["supported_platforms"].any? {|platform| platform.downcase =~ only_platform} - has_test_for_platform = true - break - end - end - end - return has_test_for_platform - end - - def validate_atomic_yaml!(yaml, used_guids_file, unique_guid_array) - raise("YAML file has no elements") if yaml.nil? - - raise('`attack_technique` element is required') unless yaml.has_key?('attack_technique') - raise('`attack_technique` element must be a string') unless yaml['attack_technique'].is_a?(String) - - raise('`display_name` element is required') unless yaml.has_key?('display_name') - raise('`display_name` element must be an array') unless yaml['display_name'].is_a?(String) - - raise('`atomic_tests` element is required') unless yaml.has_key?('atomic_tests') - raise('`atomic_tests` element must be an array') unless yaml['atomic_tests'].is_a?(Array) - raise('`atomic_tests` element is empty - you have no tests') unless yaml['atomic_tests'].count > 0 - - yaml['atomic_tests'].each_with_index do |atomic, i| - raise("`atomic_tests[#{i}].name` element is required") unless atomic.has_key?('name') - raise("`atomic_tests[#{i}].name` element must be a string") unless atomic['name'].is_a?(String) - - if atomic.has_key?('auto_generated_guid') - guid = atomic["auto_generated_guid"].to_s - raise("`atomic_tests[#{i}].auto_generated_guid` element not a proper guid") unless /[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}/.match(guid) - raise("`atomic_tests[#{i}].auto_generated_guid` element must be unique") unless !unique_guid_array.include?(guid) - unique_guid_array << guid - end - - raise("`atomic_tests[#{i}].description` element is required") unless atomic.has_key?('description') - raise("`atomic_tests[#{i}].description` element must be a string") unless atomic['description'].is_a?(String) - - raise("`atomic_tests[#{i}].supported_platforms` element is required") unless atomic.has_key?('supported_platforms') - raise("`atomic_tests[#{i}].supported_platforms` element must be an Array (was a #{atomic['supported_platforms'].class.name})") unless atomic['supported_platforms'].is_a?(Array) - - valid_supported_platforms = ['windows', 'macos', 'linux', 'office-365', 'azure-ad', 'google-workspace', 'saas', 'iaas', 'containers', 'iaas:aws', 'iaas:azure', 'iaas:gcp'] - atomic['supported_platforms'].each do |platform| - if !valid_supported_platforms.include?(platform) - raise("`atomic_tests[#{i}].supported_platforms` '#{platform}' must be one of #{valid_supported_platforms.join(', ')}") - end - end - - if atomic['dependencies'] - atomic['dependencies'].each do |dependency| - raise("`atomic_tests[#{i}].dependencies` '#{dependency}' must be have a description}") unless dependency.has_key?('description') - raise("`atomic_tests[#{i}].dependencies` '#{dependency}' must be have a prereq_command}") unless dependency.has_key?('prereq_command') - raise("`atomic_tests[#{i}].dependencies` '#{dependency}' must be have a get_prereq_command}") unless dependency.has_key?('get_prereq_command') - end - end - (atomic['input_arguments'] || {}).each_with_index do |arg_kvp, iai| - arg_name, arg = arg_kvp - raise("`atomic_tests[#{i}].input_arguments[#{iai}].description` element is required") unless arg.has_key?('description') - raise("`atomic_tests[#{i}].input_arguments[#{iai}].description` element must be a string") unless arg['description'].is_a?(String) - - raise("`atomic_tests[#{i}].input_arguments[#{iai}].type` element is required") unless arg.has_key?('type') - raise("`atomic_tests[#{i}].input_arguments[#{iai}].type` element must be a string") unless arg['type'].is_a?(String) - raise("`atomic_tests[#{i}].input_arguments[#{iai}].type` element must be lowercased and underscored (was #{arg['type']})") unless arg['type'] =~ /[a-z_]+/ - - # TODO: determine if we think default values are required for EVERY input argument - # raise("`atomic_tests[#{i}].input_arguments[#{iai}].default` element is required") unless arg.has_key?('default') - # raise("`atomic_tests[#{i}].input_arguments[#{iai}].default` element must be a string (was a #{arg['default'].class.name})") unless arg['default'].is_a?(String) - end - - raise("`atomic_tests[#{i}].executor` element is required") unless atomic.has_key?('executor') - executor = atomic['executor'] - raise("`atomic_tests[#{i}].executor.name` element is required") unless executor.has_key?('name') - raise("`atomic_tests[#{i}].executor.name` element must be a string") unless executor['name'].is_a?(String) - raise("`atomic_tests[#{i}].executor.name` element must be lowercased and underscored (was #{executor['name']})") unless executor['name'] =~ /[a-z_]+/ - - valid_executor_types = ['command_prompt', 'sh', 'bash', 'powershell', 'manual', 'aws', 'az', 'gcloud', 'kubectl'] - case executor['name'] - when 'manual' - raise("`atomic_tests[#{i}].executor.steps` element is required") unless executor.has_key?('steps') - raise("`atomic_tests[#{i}].executor.steps` element must be a string") unless executor['steps'].is_a?(String) - - validate_input_args_vs_string! input_args: (atomic['input_arguments'] || {}).keys, - string: executor['steps'], - string_description: "atomic_tests[#{i}].executor.steps" - - when 'command_prompt', 'sh', 'bash', 'powershell', 'aws', 'az', 'gcloud', 'kubectl' - raise("`atomic_tests[#{i}].executor.command` element is required") unless executor.has_key?('command') - raise("`atomic_tests[#{i}].executor.command` element must be a string") unless executor['command'].is_a?(String) - - validate_input_args_vs_string! input_args: (atomic['input_arguments'] || {}).keys, - string: executor['command'], - string_description: "atomic_tests[#{i}].executor.command" - else - raise("`atomic_tests[#{i}].executor.name` '#{executor['name']}' must be one of #{valid_executor_types.join(', ')}") - end - - validate_no_todos!(atomic, path: "atomic_tests[#{i}]") - end - end - - def record_used_guids!(yaml, used_guids_file) - return unless !yaml.nil? - - yaml['atomic_tests'].each_with_index do |atomic, i| - next unless atomic.has_key?('auto_generated_guid') - guid = atomic["auto_generated_guid"].to_s - add_guid_to_used_guid_file(guid, used_guids_file) unless guid == '' - end - end - - def generate_guids_for_yaml!(path, used_guids_file) - text = File.read(path) - # add the "auto_generated_guid:" element after the "- name:" element if it isn't already there - text.gsub!(/(?i)(^([ \t]*-[ \t]*)name:.*$(?!\s*auto_generated_guid))/) { |m| "#{$1}\n#{$2.gsub(/-/," ")}auto_generated_guid:"} - # fill the "auto_generated_guid:" element in if it doesn't contain a guid - text.gsub!(/(?i)^([ \t]*auto_generated_guid:)(?!([ \t]*[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12})).*$/) { |m| "#{$1} #{get_unique_guid!(used_guids_file)}"} - - File.open(path, "w") { |file| file << text } - end - - # generates a unique guid and records the guid as having been used by writing it to the used_guids_file - def get_unique_guid!(used_guids_file) - new_guid = '' - 20.times do |i| # if it takes more than 20 tries to get a unique guid, there must be something else going on - new_guid = SecureRandom.uuid - break unless !is_unique_guid(new_guid, used_guids_file) - end - # add this new unique guid to the used guids file - add_guid_to_used_guid_file(new_guid, used_guids_file) - return new_guid - end - - # add guid to used guid file if it is the proper format and is not already in the file. raises an exception if guid isn't valid - def add_guid_to_used_guid_file(guid, used_guids_file) - open(used_guids_file, 'a') { |f| - raise("the GUID (#{guid}) does not match the required format for the `auto_generated_guid` element") unless /[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}/ =~ guid - f.puts guid unless !is_unique_guid(guid, used_guids_file) - } - end - - def is_unique_guid(guid, used_guids_file) - return !File.foreach(used_guids_file).grep(/#{guid}/).any? - end - - # - # Validates that the arguments (specified in "#{arg}" format) in a string - # match the input_arguments for a test - # - def validate_input_args_vs_string!(input_args:, string:, string_description:) - input_args_in_string = string.scan(/#\{([^}]+)\}/).to_a.flatten - - input_args_in_string_and_not_specced = input_args_in_string - input_args - if input_args_in_string_and_not_specced.count > 0 - raise("`#{string_description}` contains args #{input_args_in_string_and_not_specced} not in input_arguments") - end - - input_args_in_spec_not_string = input_args - input_args_in_string - if input_args_in_string_and_not_specced.count > 0 - raise("`atomic_tests[#{i}].input_arguments` contains args #{input_args_in_spec_not_string} not in command") - end - end - - # - # Recursively validates that the hash (or something) doesn't contain a TODO - # - def validate_no_todos!(hashish, path:) - if hashish.is_a? String - raise "`#{path}` contains a TODO" if hashish.include? 'TODO' - elsif hashish.is_a? Array - hashish.each_with_index do |item, i| - validate_no_todos! item, path: "#{path}[#{i}]" - end - elsif hashish.is_a? Hash - hashish.each do |k, v| - validate_no_todos! v, path: "#{path}.#{k}" - end - end - end -end diff --git a/atomic_red_team/attack_api.py b/atomic_red_team/attack_api.py new file mode 100644 index 00000000..937f8804 --- /dev/null +++ b/atomic_red_team/attack_api.py @@ -0,0 +1,238 @@ +""" +Attack API module for loading and querying MITRE ATT&CK technique data. + +This module provides the Attack class that loads information about ATT&CK techniques +from MITRE's ATT&CK STIX representation using the mitreattack-python library. +""" + +import json +import re +from pathlib import Path +from typing import Dict, List, Optional, Pattern + +# Tactics in the order that the ATT&CK matrix uses +ORDERED_TACTICS = [ + "initial-access", + "execution", + "persistence", + "privilege-escalation", + "defense-evasion", + "credential-access", + "discovery", + "lateral-movement", + "collection", + "exfiltration", + "command-and-control", + "impact", +] + + +class Attack: + """ + API class that loads information about ATT&CK techniques from MITRE's ATT&CK + STIX representation. Optimized for speed with caching. + """ + + def __init__(self, stix_file: Optional[str] = None): + """ + Initialize the Attack API. + + Args: + stix_file: Optional path to a local STIX JSON file. + Defaults to enterprise-attack.json in the same directory. + """ + if stix_file is None: + stix_file = str(Path(__file__).parent / "enterprise-attack.json") + self._stix_file = stix_file + self._techniques: Optional[List[dict]] = None + self._technique_by_id: Optional[Dict[str, dict]] = None + self._attack_stix: Optional[dict] = None + + def _load_stix(self) -> dict: + """Load and cache the STIX JSON data.""" + if self._attack_stix is None: + with open(self._stix_file, "r", encoding="utf-8") as f: + self._attack_stix = json.load(f) + return self._attack_stix + + @property + def ordered_tactics(self) -> List[str]: + """Returns tactics in the order that the ATT&CK matrix uses.""" + return ORDERED_TACTICS + + def technique_identifier_for_technique(self, technique: dict) -> str: + """ + Returns the technique identifier (e.g., T1234) for a Technique object. + + Args: + technique: A technique dictionary from the STIX data. + + Returns: + The technique ID (e.g., "T1234" or "T1234.001"). + """ + external_refs = technique.get("external_references", []) + for ref in external_refs: + if ref.get("source_name") == "mitre-attack": + return ref.get("external_id", "").upper() + return "" + + def _build_technique_index(self) -> Dict[str, dict]: + """Build an index of technique_id -> technique for fast lookups.""" + if self._technique_by_id is None: + self._technique_by_id = {} + for technique in self.techniques: + tech_id = self.technique_identifier_for_technique(technique) + if tech_id: + self._technique_by_id[tech_id] = technique + return self._technique_by_id + + def technique_info(self, technique_id: str) -> Optional[dict]: + """ + Returns a Technique object given a technique identifier (T1234). + + Args: + technique_id: The technique ID (e.g., "T1234"). + + Returns: + The technique dictionary or None if not found. + """ + index = self._build_technique_index() + return index.get(technique_id.upper()) + + def ordered_tactic_to_technique_matrix( + self, only_platform: Pattern = re.compile(r".*") + ) -> List[List[Optional[dict]]]: + """ + Returns the ATT&CK Matrix as a 2D array, in order by ordered_tactics. + + Args: + only_platform: Regex pattern to filter techniques by platform. + + Returns: + 2D list of techniques organized by tactic columns. + """ + all_techniques = self.techniques_by_tactic(only_platform=only_platform) + + # Make a 2D array of techniques in the order our tactics appear + all_techniques_in_tactic_order = [] + for tactic in self.ordered_tactics: + all_techniques_in_tactic_order.append(all_techniques.get(tactic, [])) + + # Figure out the max number of techniques any one tactic has + max_techniques = ( + max(len(techs) for techs in all_techniques_in_tactic_order) + if all_techniques_in_tactic_order + else 0 + ) + + if max_techniques == 0: + return [] + + # Extend each array of techniques to that length + for techniques in all_techniques_in_tactic_order: + techniques.extend([None] * (max_techniques - len(techniques))) + + # Transpose to give us the data in columnar format + return list(map(list, zip(*all_techniques_in_tactic_order))) + + def techniques_by_tactic( + self, only_platform: Pattern = re.compile(r".*") + ) -> Dict[str, List[dict]]: + """ + Returns a map of all [ATT&CK Tactic name] => [List of ATT&CK techniques]. + + Args: + only_platform: Regex pattern to filter techniques by platform. + + Returns: + Dictionary mapping tactic names to lists of techniques. + """ + techniques_by_tactic: Dict[str, List[dict]] = {} + + for technique in self.techniques: + platforms = technique.get("x_mitre_platforms") + if platforms is None: + continue + + # Check if any platform matches + platform_match = any( + only_platform.match(p.lower().replace(" ", "-")) for p in platforms + ) + if not platform_match: + continue + + # Skip revoked or deprecated techniques + if technique.get("revoked", False): + continue + if technique.get("x_mitre_deprecated", False): + continue + + # Add to each tactic this technique belongs to + kill_chain_phases = technique.get("kill_chain_phases", []) + for phase in kill_chain_phases: + if phase.get("kill_chain_name") == "mitre-attack": + tactic_name = phase.get("phase_name") + if tactic_name: + if tactic_name not in techniques_by_tactic: + techniques_by_tactic[tactic_name] = [] + techniques_by_tactic[tactic_name].append(technique) + + return techniques_by_tactic + + @property + def techniques(self) -> List[dict]: + """ + Returns a list of all ATT&CK techniques. + + Returns: + List of technique dictionaries. + """ + if self._techniques is not None: + return self._techniques + + stix_data = self._load_stix() + self._techniques = [] + + for item in stix_data.get("objects", []): + if item.get("type") != "attack-pattern": + continue + + # Check if it has mitre-attack external reference + external_refs = item.get("external_references", []) + has_mitre_ref = any( + ref.get("source_name") == "mitre-attack" for ref in external_refs + ) + if has_mitre_ref: + self._techniques.append(item) + + return self._techniques + + def get_tactics(self) -> List[dict]: + """ + Returns a list of all ATT&CK tactics. + + Returns: + List of tactic dictionaries. + """ + stix_data = self._load_stix() + tactics = [] + for item in stix_data.get("objects", []): + if item.get("type") == "x-mitre-tactic": + tactics.append(item) + return tactics + + +# Singleton instance for convenience - lazy loaded +_attack_api: Optional[Attack] = None + + +def get_attack_api() -> Attack: + """Get or create the singleton Attack API instance.""" + global _attack_api + if _attack_api is None: + _attack_api = Attack() + return _attack_api + + +# For backwards compatibility +ATTACK_API = Attack() diff --git a/atomic_red_team/attack_api.rb b/atomic_red_team/attack_api.rb deleted file mode 100755 index 1e92b763..00000000 --- a/atomic_red_team/attack_api.rb +++ /dev/null @@ -1,119 +0,0 @@ -require 'open-uri' -require 'json' - -# -# Attack is an API class that loads information about ATT&CK techniques from MITRE'S ATT&CK -# STIX representation. It makes it very simple to do common things with ATT&CK. -# -class Attack - # - # Tactics as presented in the order that the ATT&CK matrics uses - # - def ordered_tactics - [ - 'initial-access', - 'execution', - 'persistence', - 'privilege-escalation', - 'defense-evasion', - 'credential-access', - 'discovery', - 'lateral-movement', - 'collection', - 'exfiltration', - 'command-and-control', - 'impact' - ] - end - - # - # Returns the technique identifier (T1234) for a Technique object - # - def technique_identifier_for_technique(technique) - technique.fetch('external_references', []).find do |refs| - refs['source_name'] == 'mitre-attack' - end['external_id'].upcase - end - - # - # Returns a Technique object given a technique identifier (T1234) - # - def technique_info(technique_id) - techniques.find do |item| - item.fetch('external_references', []).find do |references| - references['external_id'] == technique_id.upcase - end - end - end - - # - # Returns the ATT&CK Matrix as a 2D array, in order by `ordered_tactics` - # - def ordered_tactic_to_technique_matrix(only_platform: /.*/) - all_techniques = techniques_by_tactic(only_platform: only_platform) - - # make an 2d array of our techniques in the order our tactics appear - all_techniques_in_tactic_order = [] - ordered_tactics.each do |tactic| - all_techniques_in_tactic_order << all_techniques[tactic] - end - - # figure out the max number of techniques any one tactic has - max_techniques = all_techniques_in_tactic_order.collect(&:count).max - - # extend each array of techniques to that length - all_techniques_in_tactic_order.each {|techniques| techniques.concat(Array.new(max_techniques - techniques.count, nil))} - - # transpose to give us the data in columnar format - all_techniques_in_tactic_order.transpose - end - - # - # Returns a map of all [ ATT&CK Tactic name ] => [ List of ATT&CK techniques associated with that tactic] - # - def techniques_by_tactic(only_platform: /.*/) - techniques_by_tactic = Hash.new {|h, k| h[k] = []} - techniques.each do |technique| - next unless !technique['x_mitre_platforms'].nil? - next unless technique['x_mitre_platforms'].any? { |platform| platform.downcase.sub(" ", "-") =~ only_platform } - next unless technique.fetch('revoked', false) == false - next unless technique.fetch('x_mitre_deprecated', false) == false - - technique.fetch('kill_chain_phases', []).select { |phase| phase['kill_chain_name'] == 'mitre-attack' }.each do |tactic| - techniques_by_tactic[tactic.fetch('phase_name')] << technique - end - end - techniques_by_tactic - end - - # - # Returns a list of all ATT&CK techniques - # - def techniques - return @techniques unless @techniques.nil? - - # pull out the attack pattern objects - @techniques = attack_stix.fetch("objects").select do |item| - item.fetch('type') == 'attack-pattern' && item.fetch('external_references', []).select do |references| - references['source_name'] == 'mitre-attack' - end - end - end - - private - - # - # Returns the complete ATT&CK STIX collection parsed into a Hash - # - def attack_stix - @attack_stix ||= begin - # load the full attack library - local_attack_json_to_try = "#{File.dirname(__FILE__)}/enterprise-attack.json" - if File.exist? local_attack_json_to_try - JSON.parse File.read(local_attack_json_to_try) - else - JSON.parse open('https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json').read - end - end - end -end diff --git a/atomic_red_team/doc_generator.py b/atomic_red_team/doc_generator.py new file mode 100644 index 00000000..1dd764d0 --- /dev/null +++ b/atomic_red_team/doc_generator.py @@ -0,0 +1,729 @@ +""" +Atomic Red Team documentation generator. + +This module generates all documentation including: +- Individual technique markdown files +- ATT&CK matrices (markdown) +- Platform-specific indexes (markdown, CSV, YAML) +- ATT&CK Navigator layers (JSON) +""" + +import csv +import json +import re +from concurrent.futures import ProcessPoolExecutor, as_completed +from io import StringIO +from pathlib import Path +from typing import Dict, List, Optional, Pattern, Tuple + +from atomic_red_team.attack_api import ATTACK_API +from atomic_red_team.utils import ATOMIC_RED_TEAM, AtomicRedTeam +import yaml + +# Platform configurations for index generation +PLATFORM_CONFIGS = { + "all": {"pattern": re.compile(r".*"), "attack_pattern": re.compile(r".*")}, + "windows": { + "pattern": re.compile(r"windows"), + "attack_pattern": re.compile(r"windows"), + }, + "macos": { + "pattern": re.compile(r"macos"), + "attack_pattern": re.compile(r"windows"), + }, + "linux": { + "pattern": re.compile(r"linux"), + "attack_pattern": re.compile(r"windows"), + }, + "iaas": {"pattern": re.compile(r"iaas"), "attack_pattern": re.compile(r"windows")}, + "containers": { + "pattern": re.compile(r"containers"), + "attack_pattern": re.compile(r"windows"), + }, + "office-365": { + "pattern": re.compile(r"office-365"), + "attack_pattern": re.compile(r"office"), + }, + "google-workspace": { + "pattern": re.compile(r"google-workspace"), + "attack_pattern": re.compile(r"office"), + }, + "azure-ad": { + "pattern": re.compile(r"azure-ad"), + "attack_pattern": re.compile(r"identity"), + }, + "esxi": {"pattern": re.compile(r"esxi"), "attack_pattern": re.compile(r"esxi")}, + "iaas:gcp": { + "pattern": re.compile(r"iaas:gcp"), + "attack_pattern": re.compile(r".*"), + }, + "iaas:azure": { + "pattern": re.compile(r"iaas:azure"), + "attack_pattern": re.compile(r".*"), + }, + "iaas:aws": { + "pattern": re.compile(r"iaas:aws"), + "attack_pattern": re.compile(r".*"), + }, +} + + +def _generate_technique_doc_worker( + args: Tuple[dict, str], +) -> Tuple[str, bool, Optional[str]]: + """Standalone function for ProcessPoolExecutor to generate a single technique doc.""" + atomic_yaml, atomics_directory = args + try: + + art = AtomicRedTeam(atomics_directory=atomics_directory) + yaml_path = atomic_yaml["atomic_yaml_path"] + md_path = yaml_path.replace(".yaml", ".md") + technique_id = atomic_yaml.get("attack_technique", "").upper() + art.generate_technique_docs(technique_id, md_path) + return (yaml_path, True, None) + except Exception as ex: + return (atomic_yaml.get("atomic_yaml_path", "unknown"), False, str(ex)) + + +def _generate_matrix_worker(args: Tuple[str, str, str, Optional[str]]) -> None: + """Standalone function for ProcessPoolExecutor to generate a matrix.""" + title_prefix, output_path, atomics_directory, platform_pattern = args + import importlib + from pathlib import Path + + doc_generator = importlib.import_module('atomic_red_team.doc_generator') + utils = importlib.import_module('atomic_red_team.utils') + + art = utils.AtomicRedTeam(atomics_directory=atomics_directory) + docs = doc_generator.AtomicRedTeamDocs(atomic_red_team=art) + pattern = re.compile(platform_pattern) if platform_pattern else re.compile(r".*") + docs.generate_attack_matrix(title_prefix, Path(output_path), only_platform=pattern) + + +def _generate_index_worker( + args: Tuple[str, str, str, Optional[str], Optional[str]], +) -> None: + """Standalone function for ProcessPoolExecutor to generate a markdown index.""" + ( + title_prefix, + output_path, + atomics_directory, + only_platform_pattern, + attack_platform_pattern, + ) = args + import importlib + from pathlib import Path + + doc_generator = importlib.import_module('atomic_red_team.doc_generator') + utils = importlib.import_module('atomic_red_team.utils') + + art = utils.AtomicRedTeam(atomics_directory=atomics_directory) + docs = doc_generator.AtomicRedTeamDocs(atomic_red_team=art) + only_platform = ( + re.compile(only_platform_pattern) + if only_platform_pattern + else re.compile(r".*") + ) + attack_platform = ( + re.compile(attack_platform_pattern) + if attack_platform_pattern + else re.compile(r".*") + ) + docs.generate_index( + title_prefix, + Path(output_path), + only_platform=only_platform, + attack_platform=attack_platform, + ) + + +def _generate_index_csv_worker( + args: Tuple[str, str, Optional[str], Optional[str]], +) -> None: + """Standalone function for ProcessPoolExecutor to generate a CSV index.""" + output_path, atomics_directory, only_platform_pattern, attack_platform_pattern = ( + args + ) + import importlib + from pathlib import Path + + doc_generator = importlib.import_module('atomic_red_team.doc_generator') + utils = importlib.import_module('atomic_red_team.utils') + + art = utils.AtomicRedTeam(atomics_directory=atomics_directory) + docs = doc_generator.AtomicRedTeamDocs(atomic_red_team=art) + only_platform = ( + re.compile(only_platform_pattern) + if only_platform_pattern + else re.compile(r".*") + ) + attack_platform = ( + re.compile(attack_platform_pattern) + if attack_platform_pattern + else re.compile(r".*") + ) + docs.generate_index_csv( + Path(output_path), only_platform=only_platform, attack_platform=attack_platform + ) + + +def _generate_yaml_index_worker(args: Tuple[str, str]) -> None: + """Standalone function for ProcessPoolExecutor to generate a YAML index.""" + output_path, atomics_directory = args + import importlib + from pathlib import Path + + doc_generator = importlib.import_module('atomic_red_team.doc_generator') + utils = importlib.import_module('atomic_red_team.utils') + + art = utils.AtomicRedTeam(atomics_directory=atomics_directory) + docs = doc_generator.AtomicRedTeamDocs(atomic_red_team=art) + docs.generate_yaml_index(Path(output_path)) + + +def _generate_yaml_index_by_platform_worker(args: Tuple[str, str, str]) -> None: + """Standalone function for ProcessPoolExecutor to generate a platform-specific YAML index.""" + output_path, atomics_directory, platform = args + import importlib + from pathlib import Path + + doc_generator = importlib.import_module('atomic_red_team.doc_generator') + utils = importlib.import_module('atomic_red_team.utils') + + art = utils.AtomicRedTeam(atomics_directory=atomics_directory) + docs = doc_generator.AtomicRedTeamDocs(atomic_red_team=art) + docs.generate_yaml_index_by_platform(Path(output_path), platform) + + +class AtomicRedTeamDocs: + """ + Documentation generator for Atomic Red Team. + + Generates all documentation including technique docs, indexes, matrices, + and ATT&CK Navigator layers. + """ + + def __init__(self, atomic_red_team: Optional[AtomicRedTeam] = None): + """Initialize the documentation generator.""" + self.atomic_red_team = atomic_red_team or ATOMIC_RED_TEAM + self.atomics_directory = self.atomic_red_team.atomics_directory + + def generate_all_the_docs(self) -> Tuple[List[str], List[str]]: + """ + Generate all documentation used by Atomic Red Team. + + Returns: + Tuple of (successful_paths, failed_paths) + """ + oks = [] + fails = [] + + # Generate individual technique docs concurrently + with ProcessPoolExecutor() as executor: + future_to_yaml = { + executor.submit( + _generate_technique_doc_worker, + (atomic_yaml, self.atomics_directory), + ): atomic_yaml + for atomic_yaml in self.atomic_red_team.atomic_tests + } + + for future in as_completed(future_to_yaml): + yaml_path, success, error = future.result() + if success: + oks.append(yaml_path) + else: + fails.append(yaml_path) + print(f"✗ {yaml_path}: {error}") + + print(f"\nGenerated docs for {len(oks)} techniques, {len(fails)} failures") + + # Prepare directories + indexes_dir = Path(self.atomics_directory) / "Indexes" + matrices_dir = indexes_dir / "Matrices" + md_indexes_dir = indexes_dir / "Indexes-Markdown" + csv_indexes_dir = indexes_dir / "Indexes-CSV" + layers_dir = indexes_dir / "Attack-Navigator-Layers" + + for dir_path in [matrices_dir, md_indexes_dir, csv_indexes_dir, layers_dir]: + dir_path.mkdir(parents=True, exist_ok=True) + + print("\nGenerating indexes and matrices concurrently...") + + # Prepare all index generation tasks + tasks = [] + + # ATT&CK matrices + tasks.append(("matrix", _generate_matrix_worker, ("All", str(matrices_dir / "matrix.md"), self.atomics_directory, None))) + tasks.append(("windows-matrix", _generate_matrix_worker, ("Windows", str(matrices_dir / "windows-matrix.md"), self.atomics_directory, r"windows"))) + tasks.append(("macos-matrix", _generate_matrix_worker, ("macOS", str(matrices_dir / "macos-matrix.md"), self.atomics_directory, r"macos"))) + tasks.append(("linux-matrix", _generate_matrix_worker, ("Linux", str(matrices_dir / "linux-matrix.md"), self.atomics_directory, r"linux"))) + tasks.append(("esxi-matrix", _generate_matrix_worker, ("ESXi", str(matrices_dir / "esxi-matrix.md"), self.atomics_directory, r"esxi"))) + + # Markdown indexes + tasks.append(("md-index-all", _generate_index_worker, ("All", str(md_indexes_dir / "index.md"), self.atomics_directory, None, None))) + tasks.append(("md-index-windows", _generate_index_worker, ("Windows", str(md_indexes_dir / "windows-index.md"), self.atomics_directory, r"windows", r"windows"))) + tasks.append(("md-index-macos", _generate_index_worker, ("macOS", str(md_indexes_dir / "macos-index.md"), self.atomics_directory, r"macos", r"windows"))) + tasks.append(("md-index-linux", _generate_index_worker, ("Linux", str(md_indexes_dir / "linux-index.md"), self.atomics_directory, r"linux", r"windows"))) + tasks.append(("md-index-iaas", _generate_index_worker, ("IaaS", str(md_indexes_dir / "iaas-index.md"), self.atomics_directory, r"iaas", r"windows"))) + tasks.append(("md-index-containers", _generate_index_worker, ("Containers", str(md_indexes_dir / "containers-index.md"), self.atomics_directory, r"containers", r"windows"))) + tasks.append(("md-index-office365", _generate_index_worker, ("Office 365", str(md_indexes_dir / "office-365-index.md"), self.atomics_directory, r"office-365", r"office"))) + tasks.append(("md-index-google-workspace", _generate_index_worker, ("Google Workspace", str(md_indexes_dir / "google-workspace-index.md"), self.atomics_directory, r"google-workspace", r"office"))) + tasks.append(("md-index-azure-ad", _generate_index_worker, ("Azure AD", str(md_indexes_dir / "azure-ad-index.md"), self.atomics_directory, r"azure-ad", r"identity"))) + tasks.append(("md-index-esxi", _generate_index_worker, ("ESXi", str(md_indexes_dir / "esxi-index.md"), self.atomics_directory, r"esxi", r"esxi"))) + + # CSV indexes + tasks.append(("csv-index-all", _generate_index_csv_worker, (str(csv_indexes_dir / "index.csv"), self.atomics_directory, None, None))) + tasks.append(("csv-index-windows", _generate_index_csv_worker, (str(csv_indexes_dir / "windows-index.csv"), self.atomics_directory, r"windows", r"windows"))) + tasks.append(("csv-index-macos", _generate_index_csv_worker, (str(csv_indexes_dir / "macos-index.csv"), self.atomics_directory, r"macos", r"macos"))) + tasks.append(("csv-index-linux", _generate_index_csv_worker, (str(csv_indexes_dir / "linux-index.csv"), self.atomics_directory, r"linux", r"linux"))) + tasks.append(("csv-index-iaas", _generate_index_csv_worker, (str(csv_indexes_dir / "iaas-index.csv"), self.atomics_directory, r"iaas", r"iaas"))) + tasks.append(("csv-index-containers", _generate_index_csv_worker, (str(csv_indexes_dir / "containers-index.csv"), self.atomics_directory, r"containers", r"containers"))) + tasks.append(("csv-index-office365", _generate_index_csv_worker, (str(csv_indexes_dir / "office-365-index.csv"), self.atomics_directory, r"office-365", r"office"))) + tasks.append(("csv-index-google-workspace", _generate_index_csv_worker, (str(csv_indexes_dir / "google-workspace-index.csv"), self.atomics_directory, r"google-workspace", r"identity"))) + tasks.append(("csv-index-azure-ad", _generate_index_csv_worker, (str(csv_indexes_dir / "azure-ad-index.csv"), self.atomics_directory, r"azure-ad", r"identity"))) + tasks.append(("csv-index-esxi", _generate_index_csv_worker, (str(csv_indexes_dir / "esxi-index.csv"), self.atomics_directory, r"esxi", r"esxi"))) + + # YAML indexes + tasks.append(("yaml-index-all", _generate_yaml_index_worker, (str(indexes_dir / "index.yaml"), self.atomics_directory))) + for platform in ["windows", "macos", "linux", "office-365", "azure-ad", "google-workspace", "iaas", "containers", "iaas:gcp", "iaas:azure", "iaas:aws", "esxi"]: + filename = f"{platform.replace(':', '_')}-index.yaml" + tasks.append((f"yaml-index-{platform}", _generate_yaml_index_by_platform_worker, (str(indexes_dir / filename), self.atomics_directory, platform))) + + # Generate all indexes concurrently + with ProcessPoolExecutor() as executor: + future_to_task = {executor.submit(task[1], task[2]): task[0] for task in tasks} + + for future in as_completed(future_to_task): + task_name = future_to_task[future] + try: + future.result() + except Exception as ex: + print(f"✗ Error generating {task_name}: {ex}") + + # Generate ATT&CK Navigator layers (this is already optimized internally) + print("\nGenerating ATT&CK Navigator layers...") + self.generate_navigator_layers(layers_dir) + + return oks, fails + + def generate_attack_matrix( + self, + title_prefix: str, + output_path: Path, + only_platform: Pattern = re.compile(r".*"), + ) -> None: + """Generate a Markdown ATT&CK matrix.""" + result = f"# {title_prefix} Atomic Tests by ATT&CK Tactic & Technique\n" + result += f"| {' | '.join(ATTACK_API.ordered_tactics)} |\n" + result += f"|{'-----|' * len(ATTACK_API.ordered_tactics)}\n" + + matrix = ATTACK_API.ordered_tactic_to_technique_matrix( + only_platform=only_platform + ) + for row in matrix: + row_values = [] + for technique in row: + if technique: + row_values.append( + self.atomic_red_team.github_link_to_technique( + technique, + include_identifier=False, + only_platform=only_platform, + ) + ) + else: + row_values.append("") + result += f"| {' | '.join(row_values)} |\n" + + output_path.write_text(result, encoding="utf-8") + print(f"Generated ATT&CK matrix at {output_path}") + + def generate_index( + self, + title_prefix: str, + output_path: Path, + only_platform: Pattern = re.compile(r".*"), + attack_platform: Pattern = re.compile(r".*"), + ) -> None: + """Generate a Markdown index of ATT&CK Tactic -> Technique -> Atomic Tests.""" + result = f"# {title_prefix} Atomic Tests by ATT&CK Tactic & Technique\n" + + techniques_by_tactic = ATTACK_API.techniques_by_tactic( + only_platform=attack_platform + ) + for tactic, techniques in techniques_by_tactic.items(): + result += f"# {tactic}\n" + for technique in techniques: + result += f"- {self.atomic_red_team.github_link_to_technique(technique, include_identifier=True, only_platform=only_platform)}\n" + + atomic_tests = self.atomic_red_team.atomic_tests_for_technique( + technique + ) + for i, atomic_test in enumerate(atomic_tests): + platforms = atomic_test.get("supported_platforms", []) + if any(only_platform.match(p.lower()) for p in platforms): + result += f" - Atomic Test #{i + 1}: {atomic_test['name']} [{', '.join(platforms)}]\n" + + result += "\n" + + output_path.write_text(result, encoding="utf-8") + print(f"Generated Atomic Red Team index at {output_path}") + + def generate_index_csv( + self, + output_path: Path, + only_platform: Pattern = re.compile(r".*"), + attack_platform: Pattern = re.compile(r".*"), + ) -> None: + """Generate a CSV index.""" + output = StringIO(newline="") + writer = csv.writer(output, lineterminator="\n") + writer.writerow( + [ + "Tactic", + "Technique #", + "Technique Name", + "Test #", + "Test Name", + "Test GUID", + "Executor Name", + ] + ) + + techniques_by_tactic = ATTACK_API.techniques_by_tactic( + only_platform=attack_platform + ) + for tactic, techniques in techniques_by_tactic.items(): + for technique in techniques: + tech_id = ATTACK_API.technique_identifier_for_technique(technique) + + # Get atomic YAML to use display_name (which has full technique name for sub-techniques) + atomic_yaml = self.atomic_red_team._get_atomic_by_id(tech_id) + if not atomic_yaml: + continue + + tech_name = atomic_yaml.get("display_name", technique.get("name", "")) + + atomic_tests = self.atomic_red_team.atomic_tests_for_technique( + technique + ) + for i, atomic_test in enumerate(atomic_tests): + platforms = atomic_test.get("supported_platforms", []) + if any(only_platform.match(p.lower()) for p in platforms): + writer.writerow( + [ + tactic, + tech_id, + tech_name, + i + 1, + atomic_test.get("name", ""), + atomic_test.get("auto_generated_guid", ""), + atomic_test.get("executor", {}).get("name", ""), + ] + ) + + output_path.write_text(output.getvalue(), encoding="utf-8") + print(f"Generated Atomic Red Team CSV index at {output_path}") + + def generate_yaml_index(self, output_path: Path) -> None: + """Generate a master YAML index.""" + result: Dict[str, dict] = {} + + techniques_by_tactic = ATTACK_API.techniques_by_tactic() + for tactic, techniques in techniques_by_tactic.items(): + result[tactic] = {} + for technique in techniques: + tech_id = ATTACK_API.technique_identifier_for_technique(technique) + + # Create a copy of the technique and update name with display_name from YAML + technique_copy = json.loads(json.dumps(technique)) # Deep copy + atomic_yaml = self.atomic_red_team._get_atomic_by_id(tech_id) + if atomic_yaml and atomic_yaml.get("display_name"): + technique_copy["name"] = atomic_yaml["display_name"] + + result[tactic][tech_id] = { + "technique": technique_copy, + "atomic_tests": self.atomic_red_team.atomic_tests_for_technique( + technique + ), + } + + # Convert through JSON to eliminate YAML aliases (matching Ruby behavior) + # Use explicit_start=True to add '---' at the beginning like Ruby + yaml_content = yaml.dump( + json.loads(json.dumps(result)), + default_flow_style=False, + allow_unicode=True, + sort_keys=False, + explicit_start=True, + ) + output_path.write_text(yaml_content, encoding="utf-8") + print(f"Generated Atomic Red Team YAML index at {output_path}") + + def generate_yaml_index_by_platform(self, output_path: Path, platform: str) -> None: + """Generate a platform-specific YAML index.""" + result: Dict[str, dict] = {} + + techniques_by_tactic = ATTACK_API.techniques_by_tactic() + for tactic, techniques in techniques_by_tactic.items(): + result[tactic] = {} + for technique in techniques: + tech_id = ATTACK_API.technique_identifier_for_technique(technique) + + # Create a copy of the technique and update name with display_name from YAML + technique_copy = json.loads(json.dumps(technique)) # Deep copy + atomic_yaml = self.atomic_red_team._get_atomic_by_id(tech_id) + if atomic_yaml and atomic_yaml.get("display_name"): + technique_copy["name"] = atomic_yaml["display_name"] + + result[tactic][tech_id] = { + "technique": technique_copy, + "atomic_tests": self.atomic_red_team.atomic_tests_for_technique_by_platform( + technique, platform + ), + } + + yaml_content = yaml.dump( + json.loads(json.dumps(result)), + default_flow_style=False, + allow_unicode=True, + sort_keys=False, + explicit_start=True, + ) + output_path.write_text(yaml_content, encoding="utf-8") + print(f"Generated Atomic Red Team YAML index at {output_path}") + + def _get_layer(self, techniques: List[dict], layer_name: str) -> dict: + """Create an ATT&CK Navigator layer structure.""" + filters = {} + if "Windows" in layer_name: + filters = {"platforms": ["Windows"]} + elif "macOS" in layer_name: + filters = {"platforms": ["macOS"]} + elif "Linux" in layer_name: + filters = {"platforms": ["Linux"]} + + return { + "name": layer_name, + "versions": {"attack": "16", "navigator": "5.1.0", "layer": "4.5"}, + "description": f"{layer_name} MITRE ATT&CK Navigator Layer", + "domain": "enterprise-attack", + "filters": filters, + "gradient": { + "colors": ["#ffffff", "#ce232e"], + "minValue": 0, + "maxValue": 10, + }, + "legendItems": [ + {"label": "10 or more tests", "color": "#ce232e"}, + {"label": "1 or more tests", "color": "#ffffff"}, + ], + "techniques": techniques, + } + + def _update_techniques_list( + self, + current_technique: dict, + current_technique_parent: dict, + techniques_list: List[dict], + atomic_yaml: dict, + comments: bool, + ) -> None: + """Update the techniques list with a new technique.""" + tech_id = atomic_yaml.get("attack_technique", "") + + if "." not in tech_id: + # This is a parent technique + tech_parent = next( + ( + t + for t in techniques_list + if t["techniqueID"] == tech_id.split(".")[0] + ), + None, + ) + if tech_parent: + tech_parent["score"] += current_technique["score"] + if comments: + tech_parent["comment"] = current_technique.get("comment", "") + else: + if not comments: + current_technique.pop("comment", None) + techniques_list.append(current_technique) + else: + # This is a sub-technique + parent_id = tech_id.split(".")[0] + tech_parent = next( + (t for t in techniques_list if t["techniqueID"] == parent_id), None + ) + if tech_parent: + tech_parent["score"] += current_technique["score"] + else: + current_technique_parent["score"] += current_technique["score"] + techniques_list.append(current_technique_parent) + + if not comments: + current_technique.pop("comment", None) + techniques_list.append(current_technique) + + def generate_navigator_layers(self, output_dir: Path) -> None: + """Generate all ATT&CK Navigator layers.""" + # Initialize technique lists for each platform + platforms_data = { + "all": [], + "windows": [], + "macos": [], + "linux": [], + "iaas": [], + "iaas_aws": [], + "iaas_azure": [], + "iaas_gcp": [], + "containers": [], + "google_workspace": [], + "azure_ad": [], + "office_365": [], + "esxi": [], + } + + platform_patterns = { + "windows": re.compile(r"windows", re.I), + "macos": re.compile(r"macos", re.I), + "linux": re.compile(r"linux", re.I), + "iaas": re.compile(r"^iaas", re.I), + "iaas_aws": re.compile(r"^iaas:aws", re.I), + "iaas_azure": re.compile(r"^iaas:azure", re.I), + "iaas_gcp": re.compile(r"^iaas:gcp", re.I), + "containers": re.compile(r"^containers", re.I), + "google_workspace": re.compile(r"^google-workspace", re.I), + "azure_ad": re.compile(r"^azure-ad", re.I), + "office_365": re.compile(r"^office-365", re.I), + "esxi": re.compile(r"^esxi", re.I), + } + + for atomic_yaml in self.atomic_red_team.atomic_tests: + tech_id = atomic_yaml.get("attack_technique", "") + base_technique = { + "techniqueID": tech_id, + "score": 0, + "enabled": True, + "comment": "\n", + "links": [ + { + "label": "View Atomic", + "url": f"https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/{tech_id}/{tech_id}.md", + } + ], + } + + base_parent = { + "techniqueID": tech_id.split(".")[0], + "score": 0, + "enabled": True, + "links": [ + { + "label": "View Atomic", + "url": f"https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/{tech_id.split('.')[0]}/{tech_id.split('.')[0]}.md", + } + ], + } + + # Create platform-specific technique copies + techniques = { + key: {**base_technique, "comment": "\n"} for key in platforms_data + } + technique_parents = {key: {**base_parent} for key in platforms_data} + has_tests = {key: False for key in platforms_data} + + for atomic in atomic_yaml.get("atomic_tests", []): + techniques["all"]["score"] += 1 + supported_platforms = atomic.get("supported_platforms", []) + + for platform_key, pattern in platform_patterns.items(): + if any(pattern.match(p) for p in supported_platforms): + has_tests[platform_key] = True + techniques[platform_key]["score"] += 1 + techniques[platform_key]["comment"] += f"- {atomic['name']}\n" + + # Update the all techniques list + self._update_techniques_list( + techniques["all"], + technique_parents["all"], + platforms_data["all"], + atomic_yaml, + False, + ) + + # Update platform-specific lists + for platform_key in platform_patterns: + if has_tests[platform_key]: + self._update_techniques_list( + techniques[platform_key], + technique_parents[platform_key], + platforms_data[platform_key], + atomic_yaml, + True, + ) + + # Write layers + layer_configs = [ + ("all", "art-navigator-layer.json", "Atomic Red Team"), + ( + "windows", + "art-navigator-layer-windows.json", + "Atomic Red Team (Windows)", + ), + ("macos", "art-navigator-layer-macos.json", "Atomic Red Team (macOS)"), + ("linux", "art-navigator-layer-linux.json", "Atomic Red Team (Linux)"), + ("iaas", "art-navigator-layer-iaas.json", "Atomic Red Team (Iaas)"), + ( + "iaas_aws", + "art-navigator-layer-iaas-aws.json", + "Atomic Red Team (Iaas:AWS)", + ), + ( + "iaas_azure", + "art-navigator-layer-iaas-azure.json", + "Atomic Red Team (Iaas:Azure)", + ), + ( + "iaas_gcp", + "art-navigator-layer-iaas-gcp.json", + "Atomic Red Team (Iaas:GCP)", + ), + ( + "containers", + "art-navigator-layer-containers.json", + "Atomic Red Team (Containers)", + ), + ( + "google_workspace", + "art-navigator-layer-google-workspace.json", + "Atomic Red Team (Google-Workspace)", + ), + ( + "azure_ad", + "art-navigator-layer-azure-ad.json", + "Atomic Red Team (Azure-AD)", + ), + ( + "office_365", + "art-navigator-layer-office-365.json", + "Atomic Red Team (Office-365)", + ), + ("esxi", "art-navigator-layer-esxi.json", "Atomic Red Team (ESXi)"), + ] + + for platform_key, filename, layer_name in layer_configs: + layer = self._get_layer(platforms_data[platform_key], layer_name) + output_path = output_dir / filename + # Use separators without spaces to match Ruby's compact JSON output + output_path.write_text( + json.dumps(layer, separators=(",", ":")), encoding="utf-8" + ) + print(f"Generated Atomic Red Team ATT&CK Navigator Layer at {output_path}") + + +def generate_all_docs() -> Tuple[List[str], List[str]]: + """Generate all Atomic Red Team documentation.""" + return AtomicRedTeamDocs().generate_all_the_docs() diff --git a/atomic_red_team/models.py b/atomic_red_team/models.py index 2542dba6..4c80f750 100644 --- a/atomic_red_team/models.py +++ b/atomic_red_team/models.py @@ -134,7 +134,19 @@ class ManualExecutor(Executor): class CommandExecutor(Executor): name: Literal["powershell", "sh", "bash", "command_prompt"] command: constr(min_length=1) - cleanup_command: Optional[str] = None + cleanup_command: Optional[constr(min_length=1)] = None + + @field_validator("cleanup_command", mode="before") + @classmethod + def validate_cleanup_command(cls, v): + """Reject empty cleanup_command strings - treat them as None or error.""" + if v is not None and isinstance(v, str) and v.strip() == "": + raise PydanticCustomError( + "empty_cleanup_command", + "'cleanup_command' shouldn't be empty. Provide a valid command or remove the key from YAML", + {"loc": ["executor", "cleanup_command"], "input": v}, + ) + return v class Dependency(BaseModel): @@ -255,7 +267,10 @@ class Technique(BaseModel): "empty_dependency_executor_name", "'dependency_executor_name' shouldn't be empty. Provide a valid value ['manual','powershell', 'sh', " "'bash', 'command_prompt'] or remove the key from YAML", - {"loc": ["atomic_tests", i, "dependency_executor_name"], "input": value}, + { + "loc": ["atomic_tests", i, "dependency_executor_name"], + "input": value, + }, ) return data diff --git a/atomic_red_team/runner.py b/atomic_red_team/runner.py index b7e3c024..6f601267 100644 --- a/atomic_red_team/runner.py +++ b/atomic_red_team/runner.py @@ -5,18 +5,20 @@ import sys import urllib.parse from collections import defaultdict from functools import partial -from typing import Annotated +from pathlib import Path +from typing import Annotated, Optional import typer from pydantic import ValidationError -from atomic_red_team.common import used_guids_file, atomics_path +from atomic_red_team.common import atomics_path, used_guids_file from atomic_red_team.guid import ( generate_guids_for_yaml, get_unique_guid, ) from atomic_red_team.labels import GithubAPI from atomic_red_team.models import Technique +from atomic_red_team.utils import ATOMIC_RED_TEAM from atomic_red_team.validator import Validator, format_validation_error, yaml app = typer.Typer(help="Atomic Red Team Maintenance tool CLI helper") @@ -107,5 +109,67 @@ def validate(): sys.exit(1) +@app.command() +def generate_docs( + technique_id: Annotated[ + Optional[str], + typer.Option( + "--technique", "-t", help="Specific technique ID to generate docs for" + ), + ] = None, + output_dir: Annotated[ + Optional[str], + typer.Option("--output", "-o", help="Output directory for documentation"), + ] = None, + full: Annotated[ + bool, + typer.Option("--full", "-f", help="Generate all docs including indexes, matrices, and navigator layers"), + ] = False, +): + """Generate Markdown documentation for atomic tests. + + Use --full to generate all documentation including: + - Individual technique markdown files + - ATT&CK matrices (markdown) + - Platform-specific indexes (markdown, CSV, YAML) + - ATT&CK Navigator layers (JSON) + """ + if full: + # Generate all documentation including indexes + from atomic_red_team.doc_generator import generate_all_docs + + oks, fails = generate_all_docs() + if fails: + sys.exit(len(fails)) + return + + if output_dir is None: + output_dir = atomics_path + + if technique_id: + # Generate docs for a specific technique + technique_id = technique_id.upper() + output_path = Path(output_dir) / technique_id / f"{technique_id}.md" + try: + ATOMIC_RED_TEAM.generate_technique_docs(technique_id, str(output_path)) + print(f"Generated documentation for {technique_id} at {output_path}") + except ValueError as e: + print(f"Error: {e}") + sys.exit(1) + else: + # Generate docs for all techniques + count = 0 + for atomic_yaml in ATOMIC_RED_TEAM.atomic_tests: + tech_id = atomic_yaml.get("attack_technique", "").upper() + if tech_id: + output_path = Path(output_dir) / tech_id / f"{tech_id}.md" + try: + ATOMIC_RED_TEAM.generate_technique_docs(tech_id, str(output_path)) + count += 1 + except Exception as e: + print(f"Error generating docs for {tech_id}: {e}") + print(f"Generated documentation for {count} techniques") + + if __name__ == "__main__": app() diff --git a/atomic_red_team/utils.py b/atomic_red_team/utils.py new file mode 100644 index 00000000..ea486d82 --- /dev/null +++ b/atomic_red_team/utils.py @@ -0,0 +1,374 @@ +""" +Atomic Red Team module for loading and managing atomic tests. + +This module provides the AtomicRedTeam class that manages atomic tests, +generates documentation, and provides various utility functions. + +Optimized for speed with caching and efficient data structures. +""" + +import glob +import re +from concurrent.futures import ProcessPoolExecutor, as_completed +from functools import lru_cache +from pathlib import Path +from typing import Dict, List, Optional, Pattern, Tuple, Union + +import yaml # PyYAML is faster than ruamel.yaml for loading + +try: + from yaml import CSafeLoader as SafeLoader +except ImportError: + from yaml import SafeLoader + +from jinja2 import Environment, FileSystemLoader + +from atomic_red_team.attack_api import ATTACK_API +from atomic_red_team.common import atomics_path + +ROOT_GITHUB_URL = "https://github.com/redcanaryco/atomic-red-team" + + +@lru_cache(maxsize=1) +def _get_jinja_env() -> Environment: + """Get cached Jinja2 environment with custom filters.""" + template_dir = Path(__file__).parent + env = Environment( + loader=FileSystemLoader(template_dir), + trim_blocks=True, + lstrip_blocks=True, + auto_reload=False, # Disable auto-reload for speed + ) + # Add custom filters + env.filters["get_language"] = get_language + env.filters["cleanup"] = cleanup_for_markdown + env.filters["slugify"] = slugify + env.filters["platform_display"] = get_supported_platform_display + return env + + +@lru_cache(maxsize=1) +def _get_template(): + """Get cached compiled template.""" + return _get_jinja_env().get_template("atomic_doc_template.md.j2") + + +def get_language(executor: str) -> str: + """Convert executor name to language identifier for code blocks.""" + if executor == "command_prompt": + return "cmd" + elif executor == "manual": + return "" + return executor + + +def get_supported_platform_display(platform: str) -> str: + """Convert platform identifier to display name (matches Ruby behavior).""" + # Ruby just capitalizes the first letter, except for 'macos' -> 'macOS' + if platform == "macos": + return "macOS" + return platform.capitalize() + + +def cleanup_for_markdown(value) -> str: + """Clean up a value for use in markdown tables.""" + if value is None: + return "" + return str(value).strip().replace("\\", "\") + + +# Pre-compiled regex for slugify +_SLUGIFY_PATTERN = re.compile(r"[`~!@#$%^&*()+=<>?,.\/:;\"'|{}\[\]\\–—]") + + +def slugify(title: str) -> str: + """Convert a title to a URL-friendly slug.""" + slug = title.lower().replace(" ", "-") + return _SLUGIFY_PATTERN.sub("", slug) + + +def _load_yaml_file(path: str) -> Optional[dict]: + """Load a YAML file using fast PyYAML loader.""" + try: + with open(path, "r", encoding="utf-8") as f: + return yaml.load(f, Loader=SafeLoader) + except Exception: + return None + + +class AtomicRedTeam: + """ + Main class for managing Atomic Red Team tests. + + Provides methods for loading atomic tests, generating documentation, + and validating YAML files. Optimized for speed. + """ + + def __init__(self, atomics_directory: Optional[str] = None): + """ + Initialize the AtomicRedTeam instance. + + Args: + atomics_directory: Path to the atomics directory. + Defaults to the standard atomics path. + """ + self.atomics_directory = atomics_directory or atomics_path + self._atomic_tests: Optional[List[dict]] = None + self._atomic_tests_by_id: Optional[Dict[str, dict]] = None + self._only_platform: Pattern = re.compile(r".*") + + @property + def only_platform(self) -> Pattern: + """Get the current platform filter pattern.""" + return self._only_platform + + @only_platform.setter + def only_platform(self, pattern: Pattern): + """Set the platform filter pattern.""" + self._only_platform = pattern + + @property + def atomic_test_paths(self) -> List[str]: + """Returns a list of paths that contain Atomic Tests.""" + pattern = f"{self.atomics_directory}/T*/T*.yaml" + return sorted(glob.glob(pattern)) + + @property + def atomic_tests(self) -> List[dict]: + """ + Returns a list of Atomic Tests in Atomic Red Team (as dicts from source YAML). + """ + if self._atomic_tests is not None: + return self._atomic_tests + + self._atomic_tests = [] + for path in self.atomic_test_paths: + atomic_yaml = _load_yaml_file(path) + if atomic_yaml: + atomic_yaml["atomic_yaml_path"] = path + self._atomic_tests.append(atomic_yaml) + + return self._atomic_tests + + def _get_atomic_by_id(self, technique_id: str) -> Optional[dict]: + """Get atomic test by technique ID using cached index.""" + if self._atomic_tests_by_id is None: + self._atomic_tests_by_id = {} + for test in self.atomic_tests: + tid = test.get("attack_technique", "").upper() + if tid: + self._atomic_tests_by_id[tid] = test + return self._atomic_tests_by_id.get(technique_id.upper()) + + def atomic_tests_for_technique( + self, technique_or_identifier: Union[str, dict] + ) -> List[dict]: + """ + Returns the individual Atomic Tests for a given identifier. + + Args: + technique_or_identifier: Either a technique ID string (e.g., "T1234") + or an ATT&CK technique object. + + Returns: + List of atomic test dictionaries. + """ + if isinstance(technique_or_identifier, dict): + technique_identifier = ATTACK_API.technique_identifier_for_technique( + technique_or_identifier + ) + else: + technique_identifier = technique_or_identifier + + atomic_yaml = self._get_atomic_by_id(technique_identifier) + return atomic_yaml.get("atomic_tests", []) if atomic_yaml else [] + + def atomic_tests_for_technique_by_platform( + self, technique_or_identifier: Union[str, dict], platform: str + ) -> List[dict]: + """ + Returns the individual Atomic Tests for a given identifier filtered by platform. + + Args: + technique_or_identifier: Either a technique ID string (e.g., "T1234") + or an ATT&CK technique object. + platform: Platform to filter by (e.g., "windows", "linux", "macos"). + + Returns: + List of atomic test dictionaries matching the platform. + """ + tests = self.atomic_tests_for_technique(technique_or_identifier) + return [t for t in tests if platform in t.get("supported_platforms", [])] + + def atomic_yaml_has_test_for_platform( + self, yaml_file: str, only_platform: Pattern + ) -> bool: + """ + Check if a YAML file has tests for a given platform. + + Args: + yaml_file: Path to the YAML file. + only_platform: Regex pattern to match platforms. + + Returns: + True if the file has tests for the platform. + """ + yaml_path = Path(yaml_file) + if not yaml_path.exists(): + return False + + data = _load_yaml_file(str(yaml_path)) + if not data or "atomic_tests" not in data: + return False + + for atomic in data["atomic_tests"]: + for platform in atomic.get("supported_platforms", []): + if only_platform.match(platform.lower()): + return True + + return False + + def github_link_to_technique( + self, + technique: dict, + include_identifier: bool = False, + only_platform: Optional[Pattern] = None, + ) -> str: + """ + Returns a Markdown formatted GitHub link to a technique. + + This will be to the edit page for techniques that already have one or more + Atomic Red Team tests, or the create page for techniques that have no + existing tests for the given OS. + + Args: + technique: ATT&CK technique dictionary. + include_identifier: Whether to include the technique ID in the link text. + only_platform: Platform pattern filter. Defaults to instance's only_platform. + + Returns: + Markdown formatted link string. + """ + if only_platform is None: + only_platform = self._only_platform + + technique_identifier = ATTACK_API.technique_identifier_for_technique( + technique + ).upper() + + # Use display_name from atomic YAML if available (has full name for sub-techniques) + atomic_yaml = self._get_atomic_by_id(technique_identifier) + if atomic_yaml: + technique_name = atomic_yaml.get("display_name", technique.get("name", "")) + else: + technique_name = technique.get("name", "") + + link_display = technique_name + if include_identifier: + link_display = f"{technique_identifier} {technique_name}" + + yaml_file = f"{self.atomics_directory}/{technique_identifier}/{technique_identifier}.yaml" + markdown_file = f"{self.atomics_directory}/{technique_identifier}/{technique_identifier}.md" + + if ( + self.atomic_yaml_has_test_for_platform(yaml_file, only_platform) + and Path(markdown_file).exists() + ): + return f"[{link_display}](../../{technique_identifier}/{technique_identifier}.md)" + else: + return f"{link_display} [CONTRIBUTE A TEST](https://github.com/redcanaryco/atomic-red-team/wiki/Contributing)" + + def generate_technique_docs( + self, technique_identifier: str, output_path: Optional[str] = None + ) -> str: + """ + Generate Markdown documentation for a technique. + + Args: + technique_identifier: The technique ID (e.g., "T1059"). + output_path: Optional path to write the output. If None, returns the content. + + Returns: + The generated Markdown content. + """ + technique_identifier = technique_identifier.upper() + + # Find the atomic YAML using cached index + atomic_yaml = self._get_atomic_by_id(technique_identifier) + + if not atomic_yaml: + raise ValueError( + f"No atomic tests found for technique {technique_identifier}" + ) + + # Get technique info from ATT&CK for description + technique_info = ATTACK_API.technique_info(technique_identifier) + technique = { + "identifier": technique_identifier, + "name": atomic_yaml.get("display_name", ""), + "description": technique_info.get("description", "") if technique_info else "", + } + + # Render using cached template + template = _get_template() + content = template.render( + technique=technique, + atomic_yaml=atomic_yaml, + ) + content = content.rstrip() + "\n" + + if output_path: + Path(output_path).write_text(content, encoding="utf-8") + + return content + + def generate_all_docs(self, parallel: bool = True) -> Dict[str, str]: + """ + Generate documentation for all techniques. + + Args: + parallel: Whether to use parallel processing. + + Returns: + Dictionary mapping technique IDs to their generated documentation. + """ + docs = {} + technique_ids = [ + test.get("attack_technique", "").upper() + for test in self.atomic_tests + if test.get("attack_technique") + ] + + if parallel: + # Use parallel processing + # Create a standalone function for ProcessPoolExecutor + def _generate_doc_worker(args: Tuple[str, str]) -> Tuple[str, str]: + technique_id, atomics_directory = args + from atomic_red_team.utils import AtomicRedTeam + art = AtomicRedTeam(atomics_directory=atomics_directory) + return (technique_id, art.generate_technique_docs(technique_id)) + + with ProcessPoolExecutor() as executor: + future_to_id = { + executor.submit(_generate_doc_worker, (tid, self.atomics_directory)): tid + for tid in technique_ids + } + for future in as_completed(future_to_id): + tid = future_to_id[future] + try: + docs[tid] = future.result() + except Exception as e: + print(f"Error generating docs for {tid}: {e}") + else: + # Sequential processing + for tid in technique_ids: + try: + docs[tid] = self.generate_technique_docs(tid) + except Exception as e: + print(f"Error generating docs for {tid}: {e}") + + return docs + + +# Singleton instance for convenience +ATOMIC_RED_TEAM = AtomicRedTeam() diff --git a/atomics/T1562.004/T1562.004.md b/atomics/T1562.004/T1562.004.md index 4b5df72a..ddfd32de 100644 --- a/atomics/T1562.004/T1562.004.md +++ b/atomics/T1562.004/T1562.004.md @@ -766,10 +766,6 @@ Print the last 10 lines of the Uncomplicated Firewall (UFW) log file tail /var/log/ufw.log ``` -#### Cleanup Commands: -```sh - -``` diff --git a/atomics/T1562.004/T1562.004.yaml b/atomics/T1562.004/T1562.004.yaml index 1c59db81..372742eb 100644 --- a/atomics/T1562.004/T1562.004.yaml +++ b/atomics/T1562.004/T1562.004.yaml @@ -347,7 +347,6 @@ atomic_tests: elevation_required: true command: | tail /var/log/ufw.log - cleanup_command: | - name: Disable iptables auto_generated_guid: 7784c64e-ed0b-4b65-bf63-c86db229fd56 description: | diff --git a/poetry.lock b/poetry.lock index 770b5e71..ebadf9fb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,6 +12,17 @@ files = [ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] +[[package]] +name = "antlr4-python3-runtime" +version = "4.9.3" +description = "ANTLR 4.9.3 runtime for Python 3.7" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b"}, +] + [[package]] name = "attrs" version = "25.4.0" @@ -181,12 +192,79 @@ description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["main"] -markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "colour" +version = "0.1.5" +description = "converts and manipulates various color representation (HSL, RVB, web, X11, ...)" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "colour-0.1.5-py2.py3-none-any.whl", hash = "sha256:33f6db9d564fadc16e59921a56999b79571160ce09916303d35346dddc17978c"}, + {file = "colour-0.1.5.tar.gz", hash = "sha256:af20120fefd2afede8b001fbef2ea9da70ad7d49fafdb6489025dae8745c3aee"}, +] + +[package.extras] +test = ["nose"] + +[[package]] +name = "deepdiff" +version = "8.6.1" +description = "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "deepdiff-8.6.1-py3-none-any.whl", hash = "sha256:ee8708a7f7d37fb273a541fa24ad010ed484192cd0c4ffc0fa0ed5e2d4b9e78b"}, + {file = "deepdiff-8.6.1.tar.gz", hash = "sha256:ec56d7a769ca80891b5200ec7bd41eec300ced91ebcc7797b41eb2b3f3ff643a"}, +] + +[package.dependencies] +orderly-set = ">=5.4.1,<6" + +[package.extras] +cli = ["click (>=8.1.0,<8.2.0)", "pyyaml (>=6.0.0,<6.1.0)"] +coverage = ["coverage (>=7.6.0,<7.7.0)"] +dev = ["bump2version (>=1.0.0,<1.1.0)", "ipdb (>=0.13.0,<0.14.0)", "jsonpickle (>=4.0.0,<4.1.0)", "nox (==2025.5.1)", "numpy (>=2.0,<3.0) ; python_version < \"3.10\"", "numpy (>=2.2.0,<2.3.0) ; python_version >= \"3.10\"", "orjson (>=3.10.0,<3.11.0)", "pandas (>=2.2.0,<2.3.0)", "polars (>=1.21.0,<1.22.0)", "python-dateutil (>=2.9.0,<2.10.0)", "tomli (>=2.2.0,<2.3.0)", "tomli-w (>=1.2.0,<1.3.0)", "uuid6 (==2025.0.1)"] +docs = ["Sphinx (>=6.2.0,<6.3.0)", "sphinx-sitemap (>=2.6.0,<2.7.0)", "sphinxemoji (>=0.3.0,<0.4.0)"] +optimize = ["orjson"] +static = ["flake8 (>=7.1.0,<7.2.0)", "flake8-pyproject (>=1.2.3,<1.3.0)", "pydantic (>=2.10.0,<2.11.0)"] +test = ["pytest (>=8.3.0,<8.4.0)", "pytest-benchmark (>=5.1.0,<5.2.0)", "pytest-cov (>=6.0.0,<6.1.0)", "python-dotenv (>=1.0.0,<1.1.0)"] + +[[package]] +name = "drawsvg" +version = "2.4.0" +description = "A Python 3 library for programmatically generating SVG (vector) images and animations. Drawsvg can also render to PNG, MP4, and display your drawings in Jupyter notebook and Jupyter lab." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "drawsvg-2.4.0-py3-none-any.whl", hash = "sha256:85b54044956390f05053bc2651e2414d54a4939b57a2be0a043e5cec2e04f1bb"}, +] + +[package.extras] +all = ["cairoSVG (>=2.3,<3.0)", "imageio (>=2.5,<3.0)", "imageio-ffmpeg (>=0.4,<1.0)", "numpy (>=1.16,<2.0)", "pwkit (>=1.0,<2.0)"] +color = ["numpy (>=1.16,<2.0)", "pwkit (>=1.0,<2.0)"] +raster = ["cairoSVG (>=2.3,<3.0)", "imageio (>=2.5,<3.0)", "imageio-ffmpeg (>=0.4,<1.0)", "numpy (>=1.16,<2.0)"] + +[[package]] +name = "et-xmlfile" +version = "2.0.0" +description = "An implementation of lxml.xmlfile for the standard library" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa"}, + {file = "et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54"}, +] + [[package]] name = "hypothesis" version = "6.148.2" @@ -247,6 +325,24 @@ files = [ {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + [[package]] name = "jsonschema" version = "4.25.1" @@ -284,6 +380,41 @@ files = [ [package.dependencies] referencing = ">=0.31.0" +[[package]] +name = "loguru" +version = "0.7.3" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = "<4.0,>=3.5" +groups = ["main"] +files = [ + {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, + {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; python_version >= \"3.11\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.5.0) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v1.13.0) ; python_version >= \"3.8\"", "mypy (==v1.4.1) ; python_version == \"3.7\"", "myst-parser (==4.0.0) ; python_version >= \"3.11\"", "pre-commit (==4.0.1) ; python_version >= \"3.9\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==8.3.2) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==5.0.0) ; python_version == \"3.8\"", "pytest-cov (==6.0.0) ; python_version >= \"3.9\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.1.0) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==3.0.2) ; python_version >= \"3.11\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.23.2) ; python_version >= \"3.8\"", "twine (==6.0.1) ; python_version >= \"3.11\""] + +[[package]] +name = "markdown" +version = "3.10" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c"}, + {file = "markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e"}, +] + +[package.extras] +docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -308,6 +439,105 @@ profiling = ["gprof2dot"] rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] +[[package]] +name = "markupsafe" +version = "3.0.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -320,6 +550,157 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "mitreattack-python" +version = "5.3.0" +description = "MITRE ATT&CK python library" +optional = false +python-versions = "<4.0,>=3.11" +groups = ["main"] +files = [ + {file = "mitreattack_python-5.3.0-py3-none-any.whl", hash = "sha256:1087dbf3ffffc31f4db196104fb099d4d12a2326ebdc7a310d212afb8895b420"}, + {file = "mitreattack_python-5.3.0.tar.gz", hash = "sha256:ee28a4957224266e27bd7e420adbac5be0c94c9500e0bbe77b6d3b81ef545c89"}, +] + +[package.dependencies] +colour = ">=0.1.5" +deepdiff = ">=6.6.0" +drawsvg = ">=2.4.0" +loguru = ">=0.6.0" +Markdown = ">=3.5" +numpy = ">=1.26.1" +openpyxl = ">=3.1.2" +pandas = ">=2.1.1" +Pillow = ">=10.1.0" +pooch = ">=1.7.0" +python-dateutil = ">=2.8.2" +requests = ">=2.31.0" +rich = ">=13.6.0" +stix2 = ">=3.0.1" +tabulate = ">=0.9.0" +tqdm = ">=4.66.1" +typer = ">=0.9.0" +wheel = ">=0.41.2" +xlsxwriter = ">=3.1.8" + +[[package]] +name = "numpy" +version = "2.3.5" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.11" +groups = ["main"] +files = [ + {file = "numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10"}, + {file = "numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218"}, + {file = "numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d"}, + {file = "numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5"}, + {file = "numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7"}, + {file = "numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4"}, + {file = "numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e"}, + {file = "numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748"}, + {file = "numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c"}, + {file = "numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c"}, + {file = "numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa"}, + {file = "numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e"}, + {file = "numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769"}, + {file = "numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5"}, + {file = "numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4"}, + {file = "numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d"}, + {file = "numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28"}, + {file = "numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b"}, + {file = "numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c"}, + {file = "numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952"}, + {file = "numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa"}, + {file = "numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013"}, + {file = "numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff"}, + {file = "numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188"}, + {file = "numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0"}, + {file = "numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903"}, + {file = "numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d"}, + {file = "numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017"}, + {file = "numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf"}, + {file = "numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce"}, + {file = "numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e"}, + {file = "numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b"}, + {file = "numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae"}, + {file = "numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd"}, + {file = "numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f"}, + {file = "numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a"}, + {file = "numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139"}, + {file = "numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e"}, + {file = "numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9"}, + {file = "numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946"}, + {file = "numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1"}, + {file = "numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3"}, + {file = "numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234"}, + {file = "numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7"}, + {file = "numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82"}, + {file = "numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0"}, + {file = "numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63"}, + {file = "numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9"}, + {file = "numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b"}, + {file = "numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520"}, + {file = "numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c"}, + {file = "numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8"}, + {file = "numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248"}, + {file = "numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e"}, + {file = "numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2"}, + {file = "numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41"}, + {file = "numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad"}, + {file = "numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39"}, + {file = "numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20"}, + {file = "numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52"}, + {file = "numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b"}, + {file = "numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3"}, + {file = "numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227"}, + {file = "numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5"}, + {file = "numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf"}, + {file = "numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42"}, + {file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310"}, + {file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c"}, + {file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18"}, + {file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff"}, + {file = "numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb"}, + {file = "numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7"}, + {file = "numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425"}, + {file = "numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0"}, +] + +[[package]] +name = "openpyxl" +version = "3.1.5" +description = "A Python library to read/write Excel 2010 xlsx/xlsm files" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2"}, + {file = "openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050"}, +] + +[package.dependencies] +et-xmlfile = "*" + +[[package]] +name = "orderly-set" +version = "5.5.0" +description = "Orderly set" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "orderly_set-5.5.0-py3-none-any.whl", hash = "sha256:46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7"}, + {file = "orderly_set-5.5.0.tar.gz", hash = "sha256:e87185c8e4d8afa64e7f8160ee2c542a475b738bc891dc3f58102e654125e6ce"}, +] + +[package.extras] +coverage = ["coverage (>=7.6.0,<7.7.0)"] +dev = ["bump2version (>=1.0.0,<1.1.0)", "ipdb (>=0.13.0,<0.14.0)"] +optimize = ["orjson"] +static = ["flake8 (>=7.1.0,<7.2.0)", "flake8-pyproject (>=1.2.3,<1.3.0)"] +test = ["pytest (>=8.3.0,<8.4.0)", "pytest-benchmark (>=5.1.0,<5.2.0)", "pytest-cov (>=6.0.0,<6.1.0)", "python-dotenv (>=1.0.0,<1.1.0)"] + [[package]] name = "packaging" version = "25.0" @@ -332,6 +713,231 @@ files = [ {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] +[[package]] +name = "pandas" +version = "2.3.3" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c"}, + {file = "pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a"}, + {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1"}, + {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838"}, + {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250"}, + {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4"}, + {file = "pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826"}, + {file = "pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523"}, + {file = "pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45"}, + {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66"}, + {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b"}, + {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791"}, + {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151"}, + {file = "pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c"}, + {file = "pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53"}, + {file = "pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35"}, + {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908"}, + {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89"}, + {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98"}, + {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084"}, + {file = "pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b"}, + {file = "pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713"}, + {file = "pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8"}, + {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d"}, + {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac"}, + {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c"}, + {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493"}, + {file = "pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee"}, + {file = "pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5"}, + {file = "pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21"}, + {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78"}, + {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110"}, + {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86"}, + {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc"}, + {file = "pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0"}, + {file = "pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593"}, + {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c"}, + {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b"}, + {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6"}, + {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3"}, + {file = "pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5"}, + {file = "pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec"}, + {file = "pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7"}, + {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450"}, + {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5"}, + {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788"}, + {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87"}, + {file = "pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2"}, + {file = "pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8"}, + {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff"}, + {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29"}, + {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73"}, + {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9"}, + {file = "pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa"}, + {file = "pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + +[[package]] +name = "pillow" +version = "12.0.0" +description = "Python Imaging Library (fork)" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "pillow-12.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b"}, + {file = "pillow-12.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1"}, + {file = "pillow-12.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363"}, + {file = "pillow-12.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca"}, + {file = "pillow-12.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e"}, + {file = "pillow-12.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782"}, + {file = "pillow-12.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10"}, + {file = "pillow-12.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa"}, + {file = "pillow-12.0.0-cp310-cp310-win32.whl", hash = "sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275"}, + {file = "pillow-12.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d"}, + {file = "pillow-12.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7"}, + {file = "pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc"}, + {file = "pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257"}, + {file = "pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642"}, + {file = "pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3"}, + {file = "pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c"}, + {file = "pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227"}, + {file = "pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b"}, + {file = "pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e"}, + {file = "pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739"}, + {file = "pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e"}, + {file = "pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d"}, + {file = "pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371"}, + {file = "pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082"}, + {file = "pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f"}, + {file = "pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d"}, + {file = "pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953"}, + {file = "pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8"}, + {file = "pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79"}, + {file = "pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba"}, + {file = "pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0"}, + {file = "pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a"}, + {file = "pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad"}, + {file = "pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643"}, + {file = "pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4"}, + {file = "pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399"}, + {file = "pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5"}, + {file = "pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b"}, + {file = "pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3"}, + {file = "pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07"}, + {file = "pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e"}, + {file = "pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344"}, + {file = "pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27"}, + {file = "pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79"}, + {file = "pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098"}, + {file = "pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905"}, + {file = "pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a"}, + {file = "pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3"}, + {file = "pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced"}, + {file = "pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b"}, + {file = "pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d"}, + {file = "pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a"}, + {file = "pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe"}, + {file = "pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee"}, + {file = "pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef"}, + {file = "pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9"}, + {file = "pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b"}, + {file = "pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47"}, + {file = "pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9"}, + {file = "pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2"}, + {file = "pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a"}, + {file = "pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b"}, + {file = "pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad"}, + {file = "pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01"}, + {file = "pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c"}, + {file = "pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e"}, + {file = "pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e"}, + {file = "pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9"}, + {file = "pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab"}, + {file = "pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b"}, + {file = "pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b"}, + {file = "pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0"}, + {file = "pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6"}, + {file = "pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6"}, + {file = "pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1"}, + {file = "pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e"}, + {file = "pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca"}, + {file = "pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925"}, + {file = "pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8"}, + {file = "pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4"}, + {file = "pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52"}, + {file = "pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a"}, + {file = "pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7"}, + {file = "pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8"}, + {file = "pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a"}, + {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197"}, + {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c"}, + {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e"}, + {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76"}, + {file = "pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5"}, + {file = "pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"] +tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] +xmp = ["defusedxml"] + +[[package]] +name = "platformdirs" +version = "4.5.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"}, + {file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"}, +] + +[package.extras] +docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] +type = ["mypy (>=1.18.2)"] + [[package]] name = "pluggy" version = "1.6.0" @@ -348,6 +954,28 @@ files = [ dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] +[[package]] +name = "pooch" +version = "1.8.2" +description = "A friend to fetch your data files" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "pooch-1.8.2-py3-none-any.whl", hash = "sha256:3529a57096f7198778a5ceefd5ac3ef0e4d06a6ddaf9fc2d609b806f25302c47"}, + {file = "pooch-1.8.2.tar.gz", hash = "sha256:76561f0de68a01da4df6af38e9955c4c9d1a5c90da73f7e40276a5728ec83d10"}, +] + +[package.dependencies] +packaging = ">=20.0" +platformdirs = ">=2.5.0" +requests = ">=2.19.0" + +[package.extras] +progress = ["tqdm (>=4.41.0,<5.0.0)"] +sftp = ["paramiko (>=2.7.0)"] +xxhash = ["xxhash (>=1.4.3)"] + [[package]] name = "pydantic" version = "2.12.3" @@ -537,6 +1165,33 @@ pygments = ">=2.7.2" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2025.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, + {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -902,6 +1557,138 @@ files = [ {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, ] +[[package]] +name = "simplejson" +version = "3.20.2" +description = "Simple, fast, extensible JSON encoder/decoder for Python" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.5" +groups = ["main"] +files = [ + {file = "simplejson-3.20.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:11847093fd36e3f5a4f595ff0506286c54885f8ad2d921dfb64a85bce67f72c4"}, + {file = "simplejson-3.20.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d291911d23b1ab8eb3241204dd54e3ec60ddcd74dfcb576939d3df327205865"}, + {file = "simplejson-3.20.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:da6d16d7108d366bbbf1c1f3274662294859c03266e80dd899fc432598115ea4"}, + {file = "simplejson-3.20.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9ddf9a07694c5bbb4856271cbc4247cc6cf48f224a7d128a280482a2f78bae3d"}, + {file = "simplejson-3.20.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:3a0d2337e490e6ab42d65a082e69473717f5cc75c3c3fb530504d3681c4cb40c"}, + {file = "simplejson-3.20.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8ba88696351ed26a8648f8378a1431223f02438f8036f006d23b4f5b572778fa"}, + {file = "simplejson-3.20.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:00bcd408a4430af99d1f8b2b103bb2f5133bb688596a511fcfa7db865fbb845e"}, + {file = "simplejson-3.20.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4fc62feb76f590ccaff6f903f52a01c58ba6423171aa117b96508afda9c210f0"}, + {file = "simplejson-3.20.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6d7286dc11af60a2f76eafb0c2acde2d997e87890e37e24590bb513bec9f1bc5"}, + {file = "simplejson-3.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c01379b4861c3b0aa40cba8d44f2b448f5743999aa68aaa5d3ef7049d4a28a2d"}, + {file = "simplejson-3.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a16b029ca25645b3bc44e84a4f941efa51bf93c180b31bd704ce6349d1fc77c1"}, + {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e22a5fb7b1437ffb057e02e1936a3bfb19084ae9d221ec5e9f4cf85f69946b6"}, + {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8b6ff02fc7b8555c906c24735908854819b0d0dc85883d453e23ca4c0445d01"}, + {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bfc1c396ad972ba4431130b42307b2321dba14d988580c1ac421ec6a6b7cee3"}, + {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a97249ee1aee005d891b5a211faf58092a309f3d9d440bc269043b08f662eda"}, + {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f1036be00b5edaddbddbb89c0f80ed229714a941cfd21e51386dc69c237201c2"}, + {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5d6f5bacb8cdee64946b45f2680afa3f54cd38e62471ceda89f777693aeca4e4"}, + {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8db6841fb796ec5af632f677abf21c6425a1ebea0d9ac3ef1a340b8dc69f52b8"}, + {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0a341f7cc2aae82ee2b31f8a827fd2e51d09626f8b3accc441a6907c88aedb7"}, + {file = "simplejson-3.20.2-cp310-cp310-win32.whl", hash = "sha256:27f9c01a6bc581d32ab026f515226864576da05ef322d7fc141cd8a15a95ce53"}, + {file = "simplejson-3.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0a63ec98a4547ff366871bf832a7367ee43d047bcec0b07b66c794e2137b476"}, + {file = "simplejson-3.20.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06190b33cd7849efc413a5738d3da00b90e4a5382fd3d584c841ac20fb828c6f"}, + {file = "simplejson-3.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ad4eac7d858947a30d2c404e61f16b84d16be79eb6fb316341885bdde864fa8"}, + {file = "simplejson-3.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b392e11c6165d4a0fde41754a0e13e1d88a5ad782b245a973dd4b2bdb4e5076a"}, + {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51eccc4e353eed3c50e0ea2326173acdc05e58f0c110405920b989d481287e51"}, + {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:306e83d7c331ad833d2d43c76a67f476c4b80c4a13334f6e34bb110e6105b3bd"}, + {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f820a6ac2ef0bc338ae4963f4f82ccebdb0824fe9caf6d660670c578abe01013"}, + {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e7a066528a5451433eb3418184f05682ea0493d14e9aae690499b7e1eb6b81"}, + {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:438680ddde57ea87161a4824e8de04387b328ad51cfdf1eaf723623a3014b7aa"}, + {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cac78470ae68b8d8c41b6fca97f5bf8e024ca80d5878c7724e024540f5cdaadb"}, + {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7524e19c2da5ef281860a3d74668050c6986be15c9dd99966034ba47c68828c2"}, + {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e9b6d845a603b2eef3394eb5e21edb8626cd9ae9a8361d14e267eb969dbe413"}, + {file = "simplejson-3.20.2-cp311-cp311-win32.whl", hash = "sha256:47d8927e5ac927fdd34c99cc617938abb3624b06ff86e8e219740a86507eb961"}, + {file = "simplejson-3.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:ba4edf3be8e97e4713d06c3d302cba1ff5c49d16e9d24c209884ac1b8455520c"}, + {file = "simplejson-3.20.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4376d5acae0d1e91e78baeba4ee3cf22fbf6509d81539d01b94e0951d28ec2b6"}, + {file = "simplejson-3.20.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f8fe6de652fcddae6dec8f281cc1e77e4e8f3575249e1800090aab48f73b4259"}, + {file = "simplejson-3.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25ca2663d99328d51e5a138f22018e54c9162438d831e26cfc3458688616eca8"}, + {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12a6b2816b6cab6c3fd273d43b1948bc9acf708272074c8858f579c394f4cbc9"}, + {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac20dc3fcdfc7b8415bfc3d7d51beccd8695c3f4acb7f74e3a3b538e76672868"}, + {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db0804d04564e70862ef807f3e1ace2cc212ef0e22deb1b3d6f80c45e5882c6b"}, + {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:979ce23ea663895ae39106946ef3d78527822d918a136dbc77b9e2b7f006237e"}, + {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a2ba921b047bb029805726800819675249ef25d2f65fd0edb90639c5b1c3033c"}, + {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:12d3d4dc33770069b780cc8f5abef909fe4a3f071f18f55f6d896a370fd0f970"}, + {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aff032a59a201b3683a34be1169e71ddda683d9c3b43b261599c12055349251e"}, + {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:30e590e133b06773f0dc9c3f82e567463df40598b660b5adf53eb1c488202544"}, + {file = "simplejson-3.20.2-cp312-cp312-win32.whl", hash = "sha256:8d7be7c99939cc58e7c5bcf6bb52a842a58e6c65e1e9cdd2a94b697b24cddb54"}, + {file = "simplejson-3.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:2c0b4a67e75b945489052af6590e7dca0ed473ead5d0f3aad61fa584afe814ab"}, + {file = "simplejson-3.20.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90d311ba8fcd733a3677e0be21804827226a57144130ba01c3c6a325e887dd86"}, + {file = "simplejson-3.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:feed6806f614bdf7f5cb6d0123cb0c1c5f40407ef103aa935cffaa694e2e0c74"}, + {file = "simplejson-3.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b1d8d7c3e1a205c49e1aee6ba907dcb8ccea83651e6c3e2cb2062f1e52b0726"}, + {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:552f55745044a24c3cb7ec67e54234be56d5d6d0e054f2e4cf4fb3e297429be5"}, + {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2da97ac65165d66b0570c9e545786f0ac7b5de5854d3711a16cacbcaa8c472d"}, + {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f59a12966daa356bf68927fca5a67bebac0033cd18b96de9c2d426cd11756cd0"}, + {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133ae2098a8e162c71da97cdab1f383afdd91373b7ff5fe65169b04167da976b"}, + {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7977640af7b7d5e6a852d26622057d428706a550f7f5083e7c4dd010a84d941f"}, + {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b530ad6d55e71fa9e93e1109cf8182f427a6355848a4ffa09f69cc44e1512522"}, + {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bd96a7d981bf64f0e42345584768da4435c05b24fd3c364663f5fbc8fabf82e3"}, + {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f28ee755fadb426ba2e464d6fcf25d3f152a05eb6b38e0b4f790352f5540c769"}, + {file = "simplejson-3.20.2-cp313-cp313-win32.whl", hash = "sha256:472785b52e48e3eed9b78b95e26a256f59bb1ee38339be3075dad799e2e1e661"}, + {file = "simplejson-3.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:a1a85013eb33e4820286139540accbe2c98d2da894b2dcefd280209db508e608"}, + {file = "simplejson-3.20.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a135941a50795c934bdc9acc74e172b126e3694fe26de3c0c1bc0b33ea17e6ce"}, + {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ba488decb18738f5d6bd082018409689ed8e74bc6c4d33a0b81af6edf1c9f4"}, + {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d81f8e982923d5e9841622ff6568be89756428f98a82c16e4158ac32b92a3787"}, + {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdad497ccb1edc5020bef209e9c3e062a923e8e6fca5b8a39f0fb34380c8a66c"}, + {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a3f1db97bcd9fb592928159af7a405b18df7e847cbcc5682a209c5b2ad5d6b1"}, + {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:215b65b0dc2c432ab79c430aa4f1e595f37b07a83c1e4c4928d7e22e6b49a748"}, + {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:ece4863171ba53f086a3bfd87f02ec3d6abc586f413babfc6cf4de4d84894620"}, + {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:4a76d7c47d959afe6c41c88005f3041f583a4b9a1783cf341887a3628a77baa0"}, + {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:e9b0523582a57d9ea74f83ecefdffe18b2b0a907df1a9cef06955883341930d8"}, + {file = "simplejson-3.20.2-cp36-cp36m-win32.whl", hash = "sha256:16366591c8e08a4ac76b81d76a3fc97bf2bcc234c9c097b48d32ea6bfe2be2fe"}, + {file = "simplejson-3.20.2-cp36-cp36m-win_amd64.whl", hash = "sha256:732cf4c4ac1a258b4e9334e1e40a38303689f432497d3caeb491428b7547e782"}, + {file = "simplejson-3.20.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6c3a98e21e5f098e4f982ef302ebb1e681ff16a5d530cfce36296bea58fe2396"}, + {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10cf9ca1363dc3711c72f4ec7c1caed2bbd9aaa29a8d9122e31106022dc175c6"}, + {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:106762f8aedf3fc3364649bfe8dc9a40bf5104f872a4d2d86bae001b1af30d30"}, + {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b21659898b7496322e99674739193f81052e588afa8b31b6a1c7733d8829b925"}, + {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78fa1db6a02bca88829f2b2057c76a1d2dc2fccb8c5ff1199e352f213e9ec719"}, + {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:156139d94b660448ec8a4ea89f77ec476597f752c2ff66432d3656704c66b40e"}, + {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:b2620ac40be04dff08854baf6f4df10272f67079f61ed1b6274c0e840f2e2ae1"}, + {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:9ccef5b5d3e3ac5d9da0a0ca1d2de8cf2b0fb56b06aa0ab79325fa4bcc5a1d60"}, + {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:f526304c2cc9fd8b8d18afacb75bc171650f83a7097b2c92ad6a431b5d7c1b72"}, + {file = "simplejson-3.20.2-cp37-cp37m-win32.whl", hash = "sha256:e0f661105398121dd48d9987a2a8f7825b8297b3b2a7fe5b0d247370396119d5"}, + {file = "simplejson-3.20.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dab98625b3d6821e77ea59c4d0e71059f8063825a0885b50ed410e5c8bd5cb66"}, + {file = "simplejson-3.20.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b8205f113082e7d8f667d6cd37d019a7ee5ef30b48463f9de48e1853726c6127"}, + {file = "simplejson-3.20.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fc8da64929ef0ff16448b602394a76fd9968a39afff0692e5ab53669df1f047f"}, + {file = "simplejson-3.20.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfe704864b5fead4f21c8d448a89ee101c9b0fc92a5f40b674111da9272b3a90"}, + {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40ca7cbe7d2f423b97ed4e70989ef357f027a7e487606628c11b79667639dc84"}, + {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cec1868b237fe9fb2d466d6ce0c7b772e005aadeeda582d867f6f1ec9710cad"}, + {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:792debfba68d8dd61085ffb332d72b9f5b38269cda0c99f92c7a054382f55246"}, + {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e022b2c4c54cb4855e555f64aa3377e3e5ca912c372fa9e3edcc90ebbad93dce"}, + {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:5de26f11d5aca575d3825dddc65f69fdcba18f6ca2b4db5cef16f41f969cef15"}, + {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:e2162b2a43614727ec3df75baeda8881ab129824aa1b49410d4b6c64f55a45b4"}, + {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e11a1d6b2f7e72ca546bdb4e6374b237ebae9220e764051b867111df83acbd13"}, + {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:daf7cd18fe99eb427fa6ddb6b437cfde65125a96dc27b93a8969b6fe90a1dbea"}, + {file = "simplejson-3.20.2-cp38-cp38-win32.whl", hash = "sha256:da795ea5f440052f4f497b496010e2c4e05940d449ea7b5c417794ec1be55d01"}, + {file = "simplejson-3.20.2-cp38-cp38-win_amd64.whl", hash = "sha256:6a4b5e7864f952fcce4244a70166797d7b8fd6069b4286d3e8403c14b88656b6"}, + {file = "simplejson-3.20.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b3bf76512ccb07d47944ebdca44c65b781612d38b9098566b4bb40f713fc4047"}, + {file = "simplejson-3.20.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:214e26acf2dfb9ff3314e65c4e168a6b125bced0e2d99a65ea7b0f169db1e562"}, + {file = "simplejson-3.20.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2fb1259ca9c385b0395bad59cdbf79535a5a84fb1988f339a49bfbc57455a35a"}, + {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c34e028a2ba8553a208ded1da5fa8501833875078c4c00a50dffc33622057881"}, + {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b538f9d9e503b0dd43af60496780cb50755e4d8e5b34e5647b887675c1ae9fee"}, + {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab998e416ded6c58f549a22b6a8847e75a9e1ef98eb9fbb2863e1f9e61a4105b"}, + {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a8f1c307edf5fbf0c6db3396c5d3471409c4a40c7a2a466fbc762f20d46601a"}, + {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5a7bbac80bdb82a44303f5630baee140aee208e5a4618e8b9fde3fc400a42671"}, + {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5ef70ec8fe1569872e5a3e4720c1e1dcb823879a3c78bc02589eb88fab920b1f"}, + {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:cb11c09c99253a74c36925d461c86ea25f0140f3b98ff678322734ddc0f038d7"}, + {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:66f7c78c6ef776f8bd9afaad455e88b8197a51e95617bcc44b50dd974a7825ba"}, + {file = "simplejson-3.20.2-cp39-cp39-win32.whl", hash = "sha256:619ada86bfe3a5aa02b8222ca6bfc5aa3e1075c1fb5b3263d24ba579382df472"}, + {file = "simplejson-3.20.2-cp39-cp39-win_amd64.whl", hash = "sha256:44a6235e09ca5cc41aa5870a952489c06aa4aee3361ae46daa947d8398e57502"}, + {file = "simplejson-3.20.2-py3-none-any.whl", hash = "sha256:3b6bb7fb96efd673eac2e4235200bfffdc2353ad12c54117e1e4e2fc485ac017"}, + {file = "simplejson-3.20.2.tar.gz", hash = "sha256:5fe7a6ce14d1c300d80d08695b7f7e633de6cd72c80644021874d985b3393649"}, +] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + [[package]] name = "sortedcontainers" version = "2.4.0" @@ -914,6 +1701,86 @@ files = [ {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] +[[package]] +name = "stix2" +version = "3.0.1" +description = "Produce and consume STIX 2 JSON content" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "stix2-3.0.1-py2.py3-none-any.whl", hash = "sha256:827acf0b5b319c1b857c9db0d54907bb438b2b32312d236c891a305ad49b0ba2"}, + {file = "stix2-3.0.1.tar.gz", hash = "sha256:2a2718dc3451c84c709990b2ca220cc39c75ed23e0864d7e8d8190a9365b0cbf"}, +] + +[package.dependencies] +pytz = "*" +requests = "*" +simplejson = "*" +stix2-patterns = ">=1.2.0" + +[package.extras] +semantic = ["haversine", "rapidfuzz"] +taxii = ["taxii2-client (>=2.3.0)"] + +[[package]] +name = "stix2-patterns" +version = "2.0.0" +description = "Validate STIX 2 Patterns." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "stix2-patterns-2.0.0.tar.gz", hash = "sha256:07750c5a5af2c758e9d2aa4dde9d8e04bcd162ac2a9b0b4c4de4481d443efa08"}, + {file = "stix2_patterns-2.0.0-py2.py3-none-any.whl", hash = "sha256:ca4d68b2db42ed99794a418388769d2676ca828e9cac0b8629e73cd3f68f6458"}, +] + +[package.dependencies] +antlr4-python3-runtime = ">=4.9.0,<4.10.0" +six = "*" + +[package.extras] +dev = ["bumpversion", "check-manifest", "coverage", "pre-commit", "pytest", "pytest-cov", "sphinx", "sphinx-prompt", "tox"] +docs = ["sphinx", "sphinx-prompt"] +test = ["coverage", "pytest", "pytest-cov"] + +[[package]] +name = "tabulate" +version = "0.9.0" +description = "Pretty-print tabular data" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, + {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, +] + +[package.extras] +widechars = ["wcwidth"] + +[[package]] +name = "tqdm" +version = "4.67.1" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] +discord = ["requests"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + [[package]] name = "typer" version = "0.20.0" @@ -959,6 +1826,18 @@ files = [ [package.dependencies] typing-extensions = ">=4.12.0" +[[package]] +name = "tzdata" +version = "2025.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +files = [ + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, +] + [[package]] name = "urllib3" version = "2.5.0" @@ -977,7 +1856,50 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "wheel" +version = "0.45.1" +description = "A built-package format for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248"}, + {file = "wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729"}, +] + +[package.extras] +test = ["pytest (>=6.0.0)", "setuptools (>=65)"] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, + {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, +] + +[package.extras] +dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] + +[[package]] +name = "xlsxwriter" +version = "3.2.9" +description = "A Python module for creating Excel XLSX files." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3"}, + {file = "xlsxwriter-3.2.9.tar.gz", hash = "sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c"}, +] + [metadata] lock-version = "2.1" python-versions = "^3.11" -content-hash = "0e4cfaa291f1dd0ebfb71b35c86b9716fb0544c2f236fc0e9cde6cd4b73c2953" +content-hash = "d226dd1f9a9f877a040180bcf867f2ce26590bd9048b587a86797a5a6f8e1989" diff --git a/pyproject.toml b/pyproject.toml index b1420903..b65f5ec1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,8 @@ pydantic = "^2.12.3" typer = "^0.20.0" hypothesis = "^6.148.2" pytest = "^9.0.1" +jinja2 = "^3.1.6" +mitreattack-python = "^5.3.0" [build-system]