Compare commits

...

2 Commits

Author SHA1 Message Date
Hare Sudhan 0bb6dd34c3 update 2025-11-28 22:36:47 -05:00
Hare Sudhan 3cb6966dee pre-commit checks 2025-11-26 00:13:06 -05:00
20 changed files with 290 additions and 254 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
name: Idea name: Idea
about: An idea for a feature or improvement to Atomic Red Team. about: An idea for a feature or improvement to Atomic Red Team.
title: 'Idea: ' title: 'Idea: '
labels: 'idea' labels: 'idea'
assignees: '' assignees: ''
+1 -1
View File
@@ -7,7 +7,7 @@ assignees: ''
--- ---
### Why the change? ### Why the change?
### A summary of the change ### A summary of the change
+9 -9
View File
@@ -5,12 +5,12 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: "pip" # See documentation for possible values - package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests directory: "/" # Location of package manifests
schedule: schedule:
interval: "weekly" interval: "weekly"
# Maintain dependencies for GitHub Actions # Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
+48 -47
View File
@@ -2,58 +2,59 @@ name: assign-labels
on: on:
workflow_run: workflow_run:
workflows: ["validate-atomics"] workflows:
- "validate-atomics"
types: types:
- completed - completed
jobs: jobs:
assign-labels: assign-labels:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: download-artifact - name: download-artifact
uses: actions/github-script@v8 uses: actions/github-script@v8
with: with:
script: | script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
run_id: context.payload.workflow_run.id, run_id: context.payload.workflow_run.id,
}); });
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "labels.json" return artifact.name == "labels.json"
})[0]; })[0];
let download = await github.rest.actions.downloadArtifact({ let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
artifact_id: matchArtifact.id, artifact_id: matchArtifact.id,
archive_format: 'zip', archive_format: 'zip',
}); });
let fs = require('fs'); let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/labels.zip`, Buffer.from(download.data)); fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/labels.zip`, Buffer.from(download.data));
- name: unzip-artifact - name: unzip-artifact
run: unzip labels.zip run: unzip labels.zip
- name: assign-labels-and-reviewers - name: assign-labels-and-reviewers
uses: actions/github-script@v8 uses: actions/github-script@v8
with: with:
script: | script: |-
let fs = require('fs'); let fs = require('fs');
const obj = JSON.parse(fs.readFileSync('./labels.json')); const obj = JSON.parse(fs.readFileSync('./labels.json'));
console.log(obj) console.log(obj)
if(obj.labels.length > 0){ if(obj.labels.length > 0){
await github.rest.issues.addLabels({ await github.rest.issues.addLabels({
issue_number: obj.pr, issue_number: obj.pr,
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
labels: obj.labels labels: obj.labels
}) })
} }
if(obj.maintainers.length > 0){ if(obj.maintainers.length > 0){
await github.rest.issues.addAssignees({ await github.rest.issues.addAssignees({
issue_number: obj.pr, issue_number: obj.pr,
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
assignees: obj.maintainers assignees: obj.maintainers
}); });
} }
+55 -54
View File
@@ -1,69 +1,70 @@
name: generate-docs name: generate-docs
on: on:
push: push:
branches: [ "master" ] branches:
- "master"
jobs: jobs:
generate-docs: generate-docs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout repo - name: checkout repo
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
token: ${{ secrets.PROTECTED_BRANCH_PUSH_TOKEN }} token: ${{ secrets.PROTECTED_BRANCH_PUSH_TOKEN }}
- name: Install poetry - name: Install poetry
run: pipx install poetry run: pipx install poetry
- uses: actions/setup-python@v6 - uses: actions/setup-python@v6
with: with:
python-version: "3.11.2" python-version: "3.11.2"
cache: "poetry" cache: "poetry"
- name: Install dependencies - name: Install dependencies
run: poetry install --no-interaction run: poetry install --no-interaction
- name: Generate shields.io URL - name: Generate shields.io URL
run: poetry run python runner.py generate-counter run: poetry run python runner.py generate-counter
id: counter id: counter
working-directory: atomic_red_team working-directory: atomic_red_team
env: env:
PYTHONPATH: ${{ github.workspace }} PYTHONPATH: ${{ github.workspace }}
- name: Update README - name: Update README
run: | run: |
echo ${{ steps.counter.outputs.result }} echo ${{ steps.counter.outputs.result }}
sed -i "s|https://img.shields.io/badge/Atomics-.*-flat.svg|${{ steps.counter.outputs.result }}|" README.md sed -i "s|https://img.shields.io/badge/Atomics-.*-flat.svg|${{ steps.counter.outputs.result }}|" README.md
shell: bash shell: bash
- name: Generate and commit unique GUIDs for each atomic test - name: Generate and commit unique GUIDs for each atomic test
run: poetry run python runner.py generate-guids run: poetry run python runner.py generate-guids
working-directory: atomic_red_team working-directory: atomic_red_team
env: env:
PYTHONPATH: ${{ github.workspace }} PYTHONPATH: ${{ github.workspace }}
- name: setup ruby - name: setup ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.0 ruby-version: 3.0
bundler-cache: true bundler-cache: true
- name: generate markdown docs for atomics - name: generate markdown docs for atomics
run: | run: |-
bin/generate-atomic-docs.rb bin/generate-atomic-docs.rb
echo "" echo ""
echo "" echo ""
git status git status
echo "" echo ""
echo "" echo ""
git diff-index HEAD -- git diff-index HEAD --
if git diff-index --quiet HEAD -- ; then if git diff-index --quiet HEAD -- ; then
echo "Not committing documentation because there are no changes" echo "Not committing documentation because there are no changes"
else else
git config credential.helper 'cache --timeout=120' git config credential.helper 'cache --timeout=120'
git config user.email "opensource@redcanary.com" git config user.email "opensource@redcanary.com"
git config user.name "Atomic Red Team doc generator" git config user.name "Atomic Red Team doc generator"
git add README.md git add README.md
git add atomics git add atomics
git commit -am "Generated docs from job=$GITHUB_JOB branch=$GITHUB_REF_NAME [ci skip]" git commit -am "Generated docs from job=$GITHUB_JOB branch=$GITHUB_REF_NAME [ci skip]"
git push origin $GITHUB_REF_NAME -f git push origin $GITHUB_REF_NAME -f
fi fi
+16 -16
View File
@@ -3,27 +3,27 @@ name: validate-python-file-changes
on: on:
pull_request: pull_request:
branches: branches:
- master - master
paths: paths:
- "atomic_red_team/**/*.py" - "atomic_red_team/**/*.py"
jobs: jobs:
validate-python-file-changes: validate-python-file-changes:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- name: checkout repo - name: checkout repo
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Install poetry - name: Install poetry
run: pipx install poetry run: pipx install poetry
- name: setup python3.11 - name: setup python3.11
uses: actions/setup-python@v6 uses: actions/setup-python@v6
id: setup-python id: setup-python
with: with:
python-version: "3.12.4" python-version: "3.12.4"
cache: "poetry" cache: "poetry"
- name: Install dependencies - name: Install dependencies
run: poetry install --no-interaction run: poetry install --no-interaction
- name: Run pytest - name: Run pytest
run: poetry run pytest atomic_red_team/tests run: poetry run pytest atomic_red_team/tests
+11 -11
View File
@@ -1,19 +1,19 @@
name: 'Close stale issues and PRs' name: 'Close stale issues and PRs'
on: on:
schedule: schedule:
- cron: '30 1 * * *' - cron: '30 1 * * *'
jobs: jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v10 - uses: actions/stale@v10
with: with:
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
stale-pr-message: 'This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.' stale-pr-message: 'This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.'
close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.' close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.'
close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.' close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.'
days-before-issue-stale: 30 days-before-issue-stale: 30
days-before-pr-stale: 45 days-before-pr-stale: 45
days-before-issue-close: 10 days-before-issue-close: 10
days-before-pr-close: 10 days-before-pr-close: 10
+58 -58
View File
@@ -3,73 +3,73 @@ name: validate-atomics
on: on:
pull_request: pull_request:
branches: branches:
- master - master
jobs: jobs:
validate-atomics: validate-atomics:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout repo - name: checkout repo
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Install poetry - name: Install poetry
run: pipx install poetry run: pipx install poetry
- name: setup python3.11 - name: setup python3.11
uses: actions/setup-python@v6 uses: actions/setup-python@v6
id: setup-python id: setup-python
with: with:
python-version: "3.11.2" python-version: "3.11.2"
cache: "poetry" cache: "poetry"
- name: Install dependencies - name: Install dependencies
run: poetry install --no-interaction run: poetry install --no-interaction
- name: validate the format of atomics tests against the spec - name: validate the format of atomics tests against the spec
run: poetry run python runner.py validate run: poetry run python runner.py validate
working-directory: atomic_red_team working-directory: atomic_red_team
env: env:
PYTHONPATH: ${{ github.workspace }} PYTHONPATH: ${{ github.workspace }}
upload: upload:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout repo - name: checkout repo
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Install poetry - name: Install poetry
run: pipx install poetry run: pipx install poetry
- name: setup python3.11 - name: setup python3.11
uses: actions/setup-python@v6 uses: actions/setup-python@v6
id: setup-python id: setup-python
with: with:
python-version: "3.11.2" python-version: "3.11.2"
cache: "poetry" cache: "poetry"
- uses: actions/github-script@v8 - uses: actions/github-script@v8
id: get_pr_number id: get_pr_number
with: with:
script: | script: |
if (context.issue.number) { if (context.issue.number) {
// Return issue number if present // Return issue number if present
return context.issue.number; return context.issue.number;
} else { } else {
// Otherwise return issue number from commit // Otherwise return issue number from commit
return ( return (
await github.rest.repos.listPullRequestsAssociatedWithCommit({ await github.rest.repos.listPullRequestsAssociatedWithCommit({
commit_sha: context.sha, commit_sha: context.sha,
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
}) })
).data[0].number; ).data[0].number;
} }
result-encoding: string result-encoding: string
- name: Install dependencies - name: Install dependencies
run: poetry install --no-interaction --no-root run: poetry install --no-interaction --no-root
- name: save labels and reviewers into a file. - name: save labels and reviewers into a file.
run: | run: |
poetry run python runner.py generate-labels --pr '${{steps.get_pr_number.outputs.result}}' --token ${{ secrets.GITHUB_TOKEN }} poetry run python runner.py generate-labels --pr '${{steps.get_pr_number.outputs.result}}' --token ${{ secrets.GITHUB_TOKEN }}
working-directory: atomic_red_team working-directory: atomic_red_team
env: env:
PYTHONPATH: ${{ github.workspace }} PYTHONPATH: ${{ github.workspace }}
- uses: actions/upload-artifact@v5 - uses: actions/upload-artifact@v5
with: with:
name: labels.json name: labels.json
path: atomic_red_team/pr/ path: atomic_red_team/pr/
+8 -8
View File
@@ -3,18 +3,18 @@ name: validate-terraform
on: on:
pull_request: pull_request:
branches: branches:
- master - master
paths: paths:
- "**/*.tf" - "**/*.tf"
jobs: jobs:
validate-terraform: validate-terraform:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: hashicorp/setup-terraform@v3 - uses: hashicorp/setup-terraform@v3
- name: Terraform fmt - name: Terraform fmt
id: fmt id: fmt
run: terraform fmt -recursive -check run: terraform fmt -recursive -check
continue-on-error: false continue-on-error: false
+21
View File
@@ -0,0 +1,21 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: check-yaml
- id: check-added-large-files
args:
- "--maxkb=10000"
- id: check-merge-conflict
- id: check-json
- id: mixed-line-ending
- id: fix-byte-order-marker
- id: debug-statements
- repo: https://github.com/google/yamlfmt
rev: v0.20.0
hooks:
- id: yamlfmt
args:
- "--verbose"
+14
View File
@@ -0,0 +1,14 @@
# yamlfmt configuration file
# See https://github.com/google/yamlfmt for more options
formatter:
indent: 2
trim_trailing_whitespace: true
eof_newline: true
force_array_style: block
retain_line_breaks_single: true
indentless_arrays: true
verbose: true
exclude:
- ".git/**"
+6 -6
View File
@@ -1,6 +1,6 @@
# Contributor Code of Conduct # Contributor Code of Conduct
Welcome to the [Atomic Red Team online community](https://atomicredteam.io/). Our goal is to foster an open, safe, and welcoming environment. As a collective, we—as contributors, maintainers, and the Open Source Projects team of Red Canary—pledge to encourage our project and community to be a harassment-free space. We invite you to collaborate, exchange thoughts or information, and engage with one another. Atomic Red Team is meant for everyone, regardless of age, personal appearance, body size, disability, nationality, race, ethnicity, gender identity and expression, level of experience or academics, religion, or sexual identity and orientation. Welcome to the [Atomic Red Team online community](https://atomicredteam.io/). Our goal is to foster an open, safe, and welcoming environment. As a collective, we—as contributors, maintainers, and the Open Source Projects team of Red Canary—pledge to encourage our project and community to be a harassment-free space. We invite you to collaborate, exchange thoughts or information, and engage with one another. Atomic Red Team is meant for everyone, regardless of age, personal appearance, body size, disability, nationality, race, ethnicity, gender identity and expression, level of experience or academics, religion, or sexual identity and orientation.
## Our Guidelines ## Our Guidelines
@@ -27,7 +27,7 @@ If you see anything that you believe breaks our community guidelines, no matter
## Enforcement & Consequences ## Enforcement & Consequences
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the Community Manager. Unacceptable behavior will not be tolerated by community members, maintainers, and Red Canary team members. The Atomic Red Team Community Manager and maintainers will review and investigate all complaints. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the Community Manager. Unacceptable behavior will not be tolerated by community members, maintainers, and Red Canary team members. The Atomic Red Team Community Manager and maintainers will review and investigate all complaints.
Anyone asked to stop unacceptable behavior is expected to comply immediately. If an Atomic Red Team community member (anyone contributing to our [GitHub Repo](https://github.com/redcanaryco/atomic-red-team) or [Community Slack](https://slack.atomicredteam.io/)) engages in unacceptable behavior, the Community Manager may take any temporary or permanent action they deem appropriate, up to and including immediate expulsion from the Atomic Red Team community without warning. Anyone asked to stop unacceptable behavior is expected to comply immediately. If an Atomic Red Team community member (anyone contributing to our [GitHub Repo](https://github.com/redcanaryco/atomic-red-team) or [Community Slack](https://slack.atomicredteam.io/)) engages in unacceptable behavior, the Community Manager may take any temporary or permanent action they deem appropriate, up to and including immediate expulsion from the Atomic Red Team community without warning.
@@ -49,10 +49,10 @@ This Code of Conduct applies to all of the Atomic Red Team, and “Atomic Family
* [Atomic Red Team GitHub](https://github.com/redcanaryco/atomic-red-team) * [Atomic Red Team GitHub](https://github.com/redcanaryco/atomic-red-team)
* **Atomic Family** * **Atomic Family**
* [Invoke-AtomicRedTeam](https://github.com/redcanaryco/invoke-atomicredteam) * [Invoke-AtomicRedTeam](https://github.com/redcanaryco/invoke-atomicredteam)
* [AtomicTestHarnesses](https://github.com/redcanaryco/atomictestharnesses) * [AtomicTestHarnesses](https://github.com/redcanaryco/atomictestharnesses)
* [Chain Reactor](https://github.com/redcanaryco/chain-reactor) * [Chain Reactor](https://github.com/redcanaryco/chain-reactor)
## Attribution ## Attribution
+19 -19
View File
@@ -10,7 +10,7 @@ class AtomicRedTeam
# TODO- should these all be relative URLs? # TODO- should these all be relative URLs?
ROOT_GITHUB_URL = "https://github.com/redcanaryco/atomic-red-team" ROOT_GITHUB_URL = "https://github.com/redcanaryco/atomic-red-team"
# #
# Returns a list of paths that contain Atomic Tests # Returns a list of paths that contain Atomic Tests
# #
@@ -19,10 +19,10 @@ class AtomicRedTeam
end end
# #
# Returns a list of Atomic Tests in Atomic Red Team (as Hashes from source YAML) # Returns a list of Atomic Tests in Atomic Red Team (as Hashes from source YAML)
# #
def atomic_tests def atomic_tests
@atomic_tests ||= atomic_test_paths.collect do |path| @atomic_tests ||= atomic_test_paths.collect do |path|
atomic_yaml = YAML.load(File.read path) atomic_yaml = YAML.load(File.read path)
atomic_yaml['atomic_yaml_path'] = path atomic_yaml['atomic_yaml_path'] = path
atomic_yaml atomic_yaml
@@ -40,7 +40,7 @@ class AtomicRedTeam
end end
test_list = Array.new test_list = Array.new
atomic_tests.find do |atomic_yaml| atomic_tests.find do |atomic_yaml|
if atomic_yaml.fetch('attack_technique').upcase == technique_identifier.upcase if atomic_yaml.fetch('attack_technique').upcase == technique_identifier.upcase
atomic_yaml['atomic_tests'].each do |a_test| atomic_yaml['atomic_tests'].each do |a_test|
if a_test["supported_platforms"].include?(platform[:platform]) if a_test["supported_platforms"].include?(platform[:platform])
@@ -62,13 +62,13 @@ class AtomicRedTeam
technique_or_technique_identifier technique_or_technique_identifier
end end
atomic_tests.find do |atomic_yaml| atomic_tests.find do |atomic_yaml|
atomic_yaml.fetch('attack_technique').upcase == technique_identifier.upcase atomic_yaml.fetch('attack_technique').upcase == technique_identifier.upcase
end.to_h.fetch('atomic_tests', []) end.to_h.fetch('atomic_tests', [])
end end
# #
# Returns a Markdown formatted Github link to a technique. This will be to the edit page for # 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 already have one or more Atomic Red Team tests, or the create page for
# techniques that have no existing tests for the given OS. # techniques that have no existing tests for the given OS.
# #
@@ -103,17 +103,17 @@ class AtomicRedTeam
def validate_atomic_yaml!(yaml, used_guids_file, unique_guid_array) def validate_atomic_yaml!(yaml, used_guids_file, unique_guid_array)
raise("YAML file has no elements") if yaml.nil? 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 is required') unless yaml.has_key?('attack_technique')
raise('`attack_technique` element must be a string') unless yaml['attack_technique'].is_a?(String) 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 is required') unless yaml.has_key?('display_name')
raise('`display_name` element must be an array') unless yaml['display_name'].is_a?(String) 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 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 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 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| 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 is required") unless atomic.has_key?('name')
raise("`atomic_tests[#{i}].name` element must be a string") unless atomic['name'].is_a?(String) raise("`atomic_tests[#{i}].name` element must be a string") unless atomic['name'].is_a?(String)
@@ -127,10 +127,10 @@ class AtomicRedTeam
raise("`atomic_tests[#{i}].description` element is required") unless atomic.has_key?('description') 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}].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 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) 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'] 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| atomic['supported_platforms'].each do |platform|
if !valid_supported_platforms.include?(platform) if !valid_supported_platforms.include?(platform)
@@ -149,22 +149,22 @@ class AtomicRedTeam
arg_name, arg = arg_kvp 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 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}].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 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 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_]+/ 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 # 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 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) # 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 end
raise("`atomic_tests[#{i}].executor` element is required") unless atomic.has_key?('executor') raise("`atomic_tests[#{i}].executor` element is required") unless atomic.has_key?('executor')
executor = atomic['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 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 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_]+/ 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'] valid_executor_types = ['command_prompt', 'sh', 'bash', 'powershell', 'manual', 'aws', 'az', 'gcloud', 'kubectl']
case executor['name'] case executor['name']
when 'manual' when 'manual'
@@ -192,7 +192,7 @@ class AtomicRedTeam
def record_used_guids!(yaml, used_guids_file) def record_used_guids!(yaml, used_guids_file)
return unless !yaml.nil? return unless !yaml.nil?
yaml['atomic_tests'].each_with_index do |atomic, i| yaml['atomic_tests'].each_with_index do |atomic, i|
next unless atomic.has_key?('auto_generated_guid') next unless atomic.has_key?('auto_generated_guid')
guid = atomic["auto_generated_guid"].to_s guid = atomic["auto_generated_guid"].to_s
@@ -201,7 +201,7 @@ class AtomicRedTeam
end end
def generate_guids_for_yaml!(path, used_guids_file) def generate_guids_for_yaml!(path, used_guids_file)
text = File.read(path) text = File.read(path)
# add the "auto_generated_guid:" element after the "- name:" element if it isn't already there # 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:"} 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 # fill the "auto_generated_guid:" element in if it doesn't contain a guid
@@ -218,7 +218,7 @@ class AtomicRedTeam
break unless !is_unique_guid(new_guid, used_guids_file) break unless !is_unique_guid(new_guid, used_guids_file)
end end
# add this new unique guid to the used guids file # add this new unique guid to the used guids file
add_guid_to_used_guid_file(new_guid, used_guids_file) add_guid_to_used_guid_file(new_guid, used_guids_file)
return new_guid return new_guid
end end
+11 -11
View File
@@ -2,14 +2,14 @@ require 'open-uri'
require 'json' require 'json'
# #
# Attack is an API class that loads information about ATT&CK techniques from MITRE'S ATT&CK # 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. # STIX representation. It makes it very simple to do common things with ATT&CK.
# #
class Attack class Attack
# #
# Tactics as presented in the order that the ATT&CK matrics uses # Tactics as presented in the order that the ATT&CK matrics uses
# #
def ordered_tactics def ordered_tactics
[ [
'initial-access', 'initial-access',
'execution', 'execution',
@@ -26,27 +26,27 @@ class Attack
] ]
end end
# #
# Returns the technique identifier (T1234) for a Technique object # Returns the technique identifier (T1234) for a Technique object
# #
def technique_identifier_for_technique(technique) def technique_identifier_for_technique(technique)
technique.fetch('external_references', []).find do |refs| technique.fetch('external_references', []).find do |refs|
refs['source_name'] == 'mitre-attack' refs['source_name'] == 'mitre-attack'
end['external_id'].upcase end['external_id'].upcase
end end
# #
# Returns a Technique object given a technique identifier (T1234) # Returns a Technique object given a technique identifier (T1234)
# #
def technique_info(technique_id) def technique_info(technique_id)
techniques.find do |item| techniques.find do |item|
item.fetch('external_references', []).find do |references| item.fetch('external_references', []).find do |references|
references['external_id'] == technique_id.upcase references['external_id'] == technique_id.upcase
end end
end end
end end
# #
# Returns the ATT&CK Matrix as a 2D array, in order by `ordered_tactics` # Returns the ATT&CK Matrix as a 2D array, in order by `ordered_tactics`
# #
def ordered_tactic_to_technique_matrix(only_platform: /.*/) def ordered_tactic_to_technique_matrix(only_platform: /.*/)
@@ -68,7 +68,7 @@ class Attack
all_techniques_in_tactic_order.transpose all_techniques_in_tactic_order.transpose
end end
# #
# Returns a map of all [ ATT&CK Tactic name ] => [ List of ATT&CK techniques associated with that tactic] # Returns a map of all [ ATT&CK Tactic name ] => [ List of ATT&CK techniques associated with that tactic]
# #
def techniques_by_tactic(only_platform: /.*/) def techniques_by_tactic(only_platform: /.*/)
@@ -84,7 +84,7 @@ class Attack
end end
end end
techniques_by_tactic techniques_by_tactic
end end
# #
# Returns a list of all ATT&CK techniques # Returns a list of all ATT&CK techniques
@@ -10,4 +10,4 @@ atomic_tests:
executor: executor:
command: | command: |
sudo -i sudo -i
name: bash name: bash
@@ -17,4 +17,4 @@ atomic_tests:
"#{gsecdump_exe}" -a "#{gsecdump_exe}" -a
name: command_prompt name: command_prompt
elevation_required: true elevation_required: true
dependency_executor_name: dependency_executor_name:
@@ -17,4 +17,4 @@ atomic_tests:
"#{gsecdump_exe}" -a "#{gsecdump_exe}" -a
name: command_prompt name: command_prompt
elevation_required: true elevation_required: true
dependency_executor_name: "bash" dependency_executor_name: "bash"
+1 -1
View File
@@ -3,7 +3,7 @@ display_name: 'Brute Force: Password Spraying'
atomic_tests: atomic_tests:
- name: Password Spray all Domain Users - name: Password Spray all Domain Users
auto_generated_guid: 90bc2e54-6c84-47a5-9439-0a2a92b4b175 auto_generated_guid: 90bc2e54-6c84-47a5-9439-0a2a92b4b175
description: description: |
CAUTION! Be very careful to not exceed the password lockout threshold for users in the domain by running this test too frequently. CAUTION! Be very careful to not exceed the password lockout threshold for users in the domain by running this test too frequently.
This atomic attempts to map the IPC$ share on one of the Domain Controllers using a password of Spring2020 for each user in the %temp%\users.txt list. This atomic attempts to map the IPC$ share on one of the Domain Controllers using a password of Spring2020 for each user in the %temp%\users.txt list.
-1
View File
@@ -34,7 +34,6 @@ atomic_tests:
- name: DDEAUTO - name: DDEAUTO
auto_generated_guid: cf91174c-4e74-414e-bec0-8d60a104d181 auto_generated_guid: cf91174c-4e74-414e-bec0-8d60a104d181
description: | description: |
TrustedSec - Unicorn - https://github.com/trustedsec/unicorn TrustedSec - Unicorn - https://github.com/trustedsec/unicorn
SensePost DDEAUTO - https://sensepost.com/blog/2017/macro-less-code-exec-in-msword/ SensePost DDEAUTO - https://sensepost.com/blog/2017/macro-less-code-exec-in-msword/
+8 -8
View File
@@ -244,7 +244,7 @@ class AtomicRedTeamDocs
"versions" => { "attack": "16", "navigator": "5.1.0", "layer": "4.5" }, "versions" => { "attack": "16", "navigator": "5.1.0", "layer": "4.5" },
"description" => layer_name + " MITRE ATT&CK Navigator Layer", "description" => layer_name + " MITRE ATT&CK Navigator Layer",
"domain" => "enterprise-attack", "domain" => "enterprise-attack",
"filters"=> filters, "filters"=> filters,
"gradient" => { "gradient" => {
"colors" => ["#ffffff", "colors" => ["#ffffff",
"#ce232e" "#ce232e"
@@ -265,11 +265,11 @@ class AtomicRedTeamDocs
"techniques" => techniques "techniques" => techniques
} }
end end
# #
# Process the current technique and update the list # Process the current technique and update the list
# #
def update_techniquesList(current_technique, current_techniqueParent, techniques_list, atomic_yaml, comments) def update_techniquesList(current_technique, current_techniqueParent, techniques_list, atomic_yaml, comments)
if not atomic_yaml['attack_technique'].include?(".") then if not atomic_yaml['attack_technique'].include?(".") then
tech_parent = techniques_list.find { |h| h["techniqueID"] == atomic_yaml['attack_technique'].split('.')[0] } tech_parent = techniques_list.find { |h| h["techniqueID"] == atomic_yaml['attack_technique'].split('.')[0] }
@@ -298,7 +298,7 @@ class AtomicRedTeamDocs
techniques_list.push(current_technique) techniques_list.push(current_technique)
end end
end end
# #
# Generates a MITRE ATT&CK Navigator Layer based on contributed techniques # Generates a MITRE ATT&CK Navigator Layer based on contributed techniques
# #
@@ -385,7 +385,7 @@ class AtomicRedTeamDocs
win_technique['score'] += 1 win_technique['score'] += 1
win_technique['comment'] += "- " + atomic['name'] + "\n" win_technique['comment'] += "- " + atomic['name'] + "\n"
end end
if atomic['supported_platforms'].any? {|platform| platform.downcase =~ /macos/} then if atomic['supported_platforms'].any? {|platform| platform.downcase =~ /macos/} then
has_macos_tests = true has_macos_tests = true
macos_technique['score'] += 1 macos_technique['score'] += 1
macos_technique['comment'] += "- " + atomic['name'] + "\n" macos_technique['comment'] += "- " + atomic['name'] + "\n"
@@ -441,7 +441,7 @@ class AtomicRedTeamDocs
esxi_technique['comment'] += "- " + atomic['name'] + "\n" esxi_technique['comment'] += "- " + atomic['name'] + "\n"
end end
end end
# Update full Atomic Layer # Update full Atomic Layer
update_techniquesList(technique, techniqueParent, techniques, atomic_yaml, false) update_techniquesList(technique, techniqueParent, techniques, atomic_yaml, false)
# Update all other Atomic Layers # Update all other Atomic Layers
@@ -483,9 +483,9 @@ class AtomicRedTeamDocs
end end
end end
end end
puts techniques_iaas_gcp puts techniques_iaas_gcp
layer = get_layer techniques, "Atomic Red Team" layer = get_layer techniques, "Atomic Red Team"
layer_win = get_layer techniques_win, "Atomic Red Team (Windows)" layer_win = get_layer techniques_win, "Atomic Red Team (Windows)"
layer_mac = get_layer techniques_mac, "Atomic Red Team (macOS)" layer_mac = get_layer techniques_mac, "Atomic Red Team (macOS)"