python conversion
This commit is contained in:
@@ -1,99 +0,0 @@
|
||||
# <%= technique['identifier'] %> - <%= technique['name'] -%>
|
||||
|
||||
## [Description from ATT&CK](https://attack.mitre.org/techniques/<%= technique['identifier'].gsub(/\./, '/') %>)
|
||||
<blockquote>
|
||||
|
||||
<%= technique['description'].gsub("%\\<", "%<") %>
|
||||
|
||||
</blockquote>
|
||||
|
||||
## 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| -%>
|
||||
<br/>
|
||||
|
||||
## 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 -%>
|
||||
|
||||
|
||||
|
||||
|
||||
<br/>
|
||||
<%- end -%>
|
||||
@@ -0,0 +1,85 @@
|
||||
# {{ technique['identifier'] }} - {{ technique['name'] }}
|
||||
## [Description from ATT&CK](https://attack.mitre.org/techniques/{{ technique['identifier'].replace('.', '/') }})
|
||||
<blockquote>
|
||||
|
||||
{{ technique['description'].replace("%\\<", "%<") }}
|
||||
|
||||
</blockquote>
|
||||
|
||||
## 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'] -%}
|
||||
<br/>
|
||||
|
||||
## 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 %}
|
||||
|
||||
|
||||
<br/>
|
||||
{% endfor -%}
|
||||
@@ -1 +0,0 @@
|
||||
TBD
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -766,10 +766,6 @@ Print the last 10 lines of the Uncomplicated Firewall (UFW) log file
|
||||
tail /var/log/ufw.log
|
||||
```
|
||||
|
||||
#### Cleanup Commands:
|
||||
```sh
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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: |
|
||||
|
||||
Generated
+924
-2
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user