Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
407ecab162 | ||
|
|
cbc1f57b21 | ||
|
|
374a9e4337 | ||
|
|
83e2f80d10 | ||
|
|
576211c4ef | ||
|
|
31fc5a31d6 | ||
|
|
eb08943d4f | ||
|
|
c36ed71353 | ||
|
|
fa52dbcf84 | ||
|
|
d412e66cea | ||
|
|
efe50d3313 | ||
|
|
1062ba995e | ||
|
|
7f93bd5b59 | ||
|
|
275d170680 | ||
|
|
6d7e10b804 | ||
|
|
25944864f7 | ||
|
|
5e84a16eba | ||
|
|
244ec163a3 | ||
|
|
dabd2174d4 | ||
|
|
f8d2b41a86 | ||
|
|
902972a1ee | ||
|
|
bddb5fbd2f | ||
|
|
adfd769963 | ||
|
|
c75e70ec74 | ||
|
|
6118183105 | ||
|
|
da755d8411 | ||
|
|
742e03d90f | ||
|
|
744228a03e | ||
|
|
5d1c6f54cd | ||
|
|
0a3dd4600b | ||
|
|
0289891d07 | ||
|
|
87cdf837e6 | ||
|
|
ea4c7d6403 | ||
|
|
2807549564 | ||
|
|
c0fe96cec6 | ||
|
|
8c967ac237 | ||
|
|
c48b46e932 | ||
|
|
49d1af7798 | ||
|
|
d44fd008ae | ||
|
|
c0c9ea3403 | ||
|
|
21359da766 | ||
|
|
e51c79c241 | ||
|
|
195bae903f | ||
|
|
5aff21a9a1 | ||
|
|
6f289d1b8e | ||
|
|
71b21aec59 | ||
|
|
42a87d4eaa | ||
|
|
51d125642f | ||
|
|
ddebf2e1cb | ||
|
|
7f3e8f1fb1 | ||
|
|
ab7dbcd2e4 | ||
|
|
7e5cbddf5d | ||
|
|
44f517c20d | ||
|
|
7bf8c6e3a1 | ||
|
|
31ea683335 | ||
|
|
29d8f1fd27 | ||
|
|
a6c472bb2a | ||
|
|
b880d419a3 | ||
|
|
a2ff87af8a | ||
|
|
5b9c577380 | ||
|
|
4775e124db | ||
|
|
c243158d7c | ||
|
|
8afc3f46f6 | ||
|
|
8b5dc54397 | ||
|
|
1dbb34df9f | ||
|
|
9383f0bc77 | ||
|
|
13306b71e0 | ||
|
|
8719a23de4 | ||
|
|
7e0b5236af | ||
|
|
c7798b3254 | ||
|
|
7d668550f5 | ||
|
|
c945eaf804 | ||
|
|
1bfe0e0874 | ||
|
|
153c6a7b01 | ||
|
|
30a83fa382 | ||
|
|
c0bcefe0bf | ||
|
|
5d16a77891 | ||
|
|
cd01a01894 | ||
|
|
df36bb9f35 | ||
|
|
030893e125 | ||
|
|
b2ab8ab54c | ||
|
|
12eb1b96de | ||
|
|
cff7d4bad4 | ||
|
|
a31c616a21 | ||
|
|
3d2b4dcc26 | ||
|
|
c7d24ee290 | ||
|
|
06c958f081 | ||
|
|
b8efe585d5 | ||
|
|
e7eb2152cc | ||
|
|
e1a8641399 | ||
|
|
cffac62e68 |
9
.gitattributes
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Set the default behavior, in case people don't have core.autocrlf set.
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
# Explicitly declare text files you want to always be normalized and converted
|
||||||
|
# to native line endings on checkout.
|
||||||
|
*.py text
|
||||||
|
*.yml text
|
||||||
|
*.md text
|
||||||
|
*.txt text
|
||||||
92
.github/CODE_OF_CONDUCT.md
vendored
@@ -1,46 +1,46 @@
|
|||||||
# Contributor Covenant Code of Conduct
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
## Our Pledge
|
## Our Pledge
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
## Our Standards
|
## Our Standards
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment include:
|
Examples of behavior that contributes to creating a positive environment include:
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
* Using welcoming and inclusive language
|
||||||
* Being respectful of differing viewpoints and experiences
|
* Being respectful of differing viewpoints and experiences
|
||||||
* Gracefully accepting constructive criticism
|
* Gracefully accepting constructive criticism
|
||||||
* Focusing on what is best for the community
|
* Focusing on what is best for the community
|
||||||
* Showing empathy towards other community members
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
* Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||||
|
|
||||||
## Our Responsibilities
|
## Our Responsibilities
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||||
|
|
||||||
## Enforcement
|
## Enforcement
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version]
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version]
|
||||||
|
|
||||||
[homepage]: https://contributor-covenant.org
|
[homepage]: https://contributor-covenant.org
|
||||||
[version]: https://contributor-covenant.org/version/1/4/
|
[version]: https://contributor-covenant.org/version/1/4/
|
||||||
|
|||||||
394
.github/CONTRIBUTING.md
vendored
@@ -1,197 +1,197 @@
|
|||||||
# Contributing to Capa
|
# Contributing to Capa
|
||||||
|
|
||||||
First off, thanks for taking the time to contribute!
|
First off, thanks for taking the time to contribute!
|
||||||
|
|
||||||
The following is a set of guidelines for contributing to capa and its packages, which are hosted in the [FireEye Organization](https://github.com/fireeye) on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
|
The following is a set of guidelines for contributing to capa and its packages, which are hosted in the [FireEye Organization](https://github.com/fireeye) on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
|
||||||
|
|
||||||
#### Table Of Contents
|
#### Table Of Contents
|
||||||
|
|
||||||
[Code of Conduct](#code-of-conduct)
|
[Code of Conduct](#code-of-conduct)
|
||||||
|
|
||||||
[What should I know before I get started?](#what-should-i-know-before-i-get-started)
|
[What should I know before I get started?](#what-should-i-know-before-i-get-started)
|
||||||
* [Capa and its Repositories](#capa-and-its-repositories)
|
* [Capa and its Repositories](#capa-and-its-repositories)
|
||||||
* [Capa Design Decisions](#design-decisions)
|
* [Capa Design Decisions](#design-decisions)
|
||||||
|
|
||||||
[How Can I Contribute?](#how-can-i-contribute)
|
[How Can I Contribute?](#how-can-i-contribute)
|
||||||
* [Reporting Bugs](#reporting-bugs)
|
* [Reporting Bugs](#reporting-bugs)
|
||||||
* [Suggesting Enhancements](#suggesting-enhancements)
|
* [Suggesting Enhancements](#suggesting-enhancements)
|
||||||
* [Your First Code Contribution](#your-first-code-contribution)
|
* [Your First Code Contribution](#your-first-code-contribution)
|
||||||
* [Pull Requests](#pull-requests)
|
* [Pull Requests](#pull-requests)
|
||||||
|
|
||||||
[Styleguides](#styleguides)
|
[Styleguides](#styleguides)
|
||||||
* [Git Commit Messages](#git-commit-messages)
|
* [Git Commit Messages](#git-commit-messages)
|
||||||
* [Python Styleguide](#python-styleguide)
|
* [Python Styleguide](#python-styleguide)
|
||||||
* [Rules Styleguide](#rules-styleguide)
|
* [Rules Styleguide](#rules-styleguide)
|
||||||
|
|
||||||
## Code of Conduct
|
## Code of Conduct
|
||||||
|
|
||||||
This project and everyone participating in it is governed by the [Capa Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to the maintainers.
|
This project and everyone participating in it is governed by the [Capa Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to the maintainers.
|
||||||
|
|
||||||
## What should I know before I get started?
|
## What should I know before I get started?
|
||||||
|
|
||||||
### Capa and its repositories
|
### Capa and its repositories
|
||||||
|
|
||||||
We host the capa project as three Github repositories:
|
We host the capa project as three Github repositories:
|
||||||
- [capa](https://github.com/fireeye/capa)
|
- [capa](https://github.com/fireeye/capa)
|
||||||
- [capa-rules](https://github.com/fireeye/capa-rules)
|
- [capa-rules](https://github.com/fireeye/capa-rules)
|
||||||
- [capa-testfiles](https://github.com/fireeye/capa-testfiles)
|
- [capa-testfiles](https://github.com/fireeye/capa-testfiles)
|
||||||
|
|
||||||
The command line tools, logic engine, and other Python source code are found in the `capa` repository.
|
The command line tools, logic engine, and other Python source code are found in the `capa` repository.
|
||||||
This is the repository to fork when you want to enhance the features, performance, or user interface of capa.
|
This is the repository to fork when you want to enhance the features, performance, or user interface of capa.
|
||||||
Do *not* push rules directly to this repository, instead...
|
Do *not* push rules directly to this repository, instead...
|
||||||
|
|
||||||
The standard rules contributed by the community are found in the `capa-rules` repository.
|
The standard rules contributed by the community are found in the `capa-rules` repository.
|
||||||
When you have an idea for a new rule, you should open a PR against `capa-rules`.
|
When you have an idea for a new rule, you should open a PR against `capa-rules`.
|
||||||
We keep `capa` and `capa-rules` separate to distinguish where ideas, bugs, and discussions should happen.
|
We keep `capa` and `capa-rules` separate to distinguish where ideas, bugs, and discussions should happen.
|
||||||
If you're writing yaml it probably goes in `capa-rules` and if you're writing Python it probably goes in `capa`.
|
If you're writing yaml it probably goes in `capa-rules` and if you're writing Python it probably goes in `capa`.
|
||||||
Also, we encourage users to develop their own rule repositories, so we treat our default set of rules in the same way.
|
Also, we encourage users to develop their own rule repositories, so we treat our default set of rules in the same way.
|
||||||
|
|
||||||
Test fixtures, such as malware samples and analysis workspaces, are found in the `capa-testfiles` repository.
|
Test fixtures, such as malware samples and analysis workspaces, are found in the `capa-testfiles` repository.
|
||||||
These are files you'll need in order to run the linter (in `--thorough` mode) and full test suites;
|
These are files you'll need in order to run the linter (in `--thorough` mode) and full test suites;
|
||||||
however, they take up a lot of space (1GB+), so by keeping `capa-testfiles` separate,
|
however, they take up a lot of space (1GB+), so by keeping `capa-testfiles` separate,
|
||||||
a shallow checkout of `capa` and `capa-rules` doesn't take much bandwidth.
|
a shallow checkout of `capa` and `capa-rules` doesn't take much bandwidth.
|
||||||
|
|
||||||
### Design Decisions
|
### Design Decisions
|
||||||
|
|
||||||
When we make a significant decision in how we maintain the project and what we can or cannot support,
|
When we make a significant decision in how we maintain the project and what we can or cannot support,
|
||||||
we will document it in the [capa issues tracker](https://github.com/fireeye/capa/issues).
|
we will document it in the [capa issues tracker](https://github.com/fireeye/capa/issues).
|
||||||
This is the best place review our discussions about what/how/why we do things in the project.
|
This is the best place review our discussions about what/how/why we do things in the project.
|
||||||
If you have a question, check to see if it is documented there.
|
If you have a question, check to see if it is documented there.
|
||||||
If it is *not* documented there, or you can't find an answer, please open a issue.
|
If it is *not* documented there, or you can't find an answer, please open a issue.
|
||||||
We'll link to existing issues when appropriate to keep discussions in one place.
|
We'll link to existing issues when appropriate to keep discussions in one place.
|
||||||
|
|
||||||
## How Can I Contribute?
|
## How Can I Contribute?
|
||||||
|
|
||||||
### Reporting Bugs
|
### Reporting Bugs
|
||||||
|
|
||||||
This section guides you through submitting a bug report for capa.
|
This section guides you through submitting a bug report for capa.
|
||||||
Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports.
|
Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports.
|
||||||
|
|
||||||
Before creating bug reports, please check [this list](#before-submitting-a-bug-report)
|
Before creating bug reports, please check [this list](#before-submitting-a-bug-report)
|
||||||
as you might find out that you don't need to create one.
|
as you might find out that you don't need to create one.
|
||||||
When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report).
|
When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report).
|
||||||
Fill out [the required template](./ISSUE_TEMPLATE/bug_report.md),
|
Fill out [the required template](./ISSUE_TEMPLATE/bug_report.md),
|
||||||
the information it asks for helps us resolve issues faster.
|
the information it asks for helps us resolve issues faster.
|
||||||
|
|
||||||
> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
|
> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
|
||||||
|
|
||||||
#### Before Submitting A Bug Report
|
#### Before Submitting A Bug Report
|
||||||
|
|
||||||
* **Determine [which repository the problem should be reported in](#capa-and-its-repositories)**.
|
* **Determine [which repository the problem should be reported in](#capa-and-its-repositories)**.
|
||||||
* **Perform a [cursory search](https://github.com/fireeye/capa/issues?q=is%3Aissue)** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one.
|
* **Perform a [cursory search](https://github.com/fireeye/capa/issues?q=is%3Aissue)** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one.
|
||||||
|
|
||||||
#### How Do I Submit A (Good) Bug Report?
|
#### How Do I Submit A (Good) Bug Report?
|
||||||
|
|
||||||
Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/).
|
Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/).
|
||||||
After you've determined [which repository](#capa-and-its-repositories) your bug is related to,
|
After you've determined [which repository](#capa-and-its-repositories) your bug is related to,
|
||||||
create an issue on that repository and provide the following information by filling in
|
create an issue on that repository and provide the following information by filling in
|
||||||
[the template](./ISSUE_TEMPLATE/bug_report.md).
|
[the template](./ISSUE_TEMPLATE/bug_report.md).
|
||||||
|
|
||||||
Explain the problem and include additional details to help maintainers reproduce the problem:
|
Explain the problem and include additional details to help maintainers reproduce the problem:
|
||||||
|
|
||||||
* **Use a clear and descriptive title** for the issue to identify the problem.
|
* **Use a clear and descriptive title** for the issue to identify the problem.
|
||||||
* **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how you started capa, e.g. which command exactly you used in the terminal, or how you started capa otherwise.
|
* **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how you started capa, e.g. which command exactly you used in the terminal, or how you started capa otherwise.
|
||||||
* **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
|
* **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
|
||||||
* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.
|
* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.
|
||||||
* **Explain which behavior you expected to see instead and why.**
|
* **Explain which behavior you expected to see instead and why.**
|
||||||
* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
|
* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
|
||||||
* **If you're reporting that capa crashed**, include the stack trace from the terminal. Include the stack trace in the issue in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines), a [file attachment](https://help.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist.
|
* **If you're reporting that capa crashed**, include the stack trace from the terminal. Include the stack trace in the issue in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines), a [file attachment](https://help.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist.
|
||||||
* **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below.
|
* **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below.
|
||||||
|
|
||||||
Provide more context by answering these questions:
|
Provide more context by answering these questions:
|
||||||
|
|
||||||
* **Did the problem start happening recently** (e.g. after updating to a new version of capa) or was this always a problem?
|
* **Did the problem start happening recently** (e.g. after updating to a new version of capa) or was this always a problem?
|
||||||
* If the problem started happening recently, **can you reproduce the problem in an older version of capa?** What's the most recent version in which the problem doesn't happen? You can download older versions of capa from [the releases page](https://github.com/fireeye/capa/releases).
|
* If the problem started happening recently, **can you reproduce the problem in an older version of capa?** What's the most recent version in which the problem doesn't happen? You can download older versions of capa from [the releases page](https://github.com/fireeye/capa/releases).
|
||||||
* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.
|
* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.
|
||||||
* If the problem is related to working with files (e.g. opening and editing files), **does the problem happen for all files and projects or only some?** Does the problem happen only when working with local or remote files (e.g. on network drives), with files of a specific type (e.g. only JavaScript or Python files), with large files or files with very long lines, or with files in a specific encoding? Is there anything else special about the files you are using?
|
* If the problem is related to working with files (e.g. opening and editing files), **does the problem happen for all files and projects or only some?** Does the problem happen only when working with local or remote files (e.g. on network drives), with files of a specific type (e.g. only JavaScript or Python files), with large files or files with very long lines, or with files in a specific encoding? Is there anything else special about the files you are using?
|
||||||
|
|
||||||
Include details about your configuration and environment:
|
Include details about your configuration and environment:
|
||||||
|
|
||||||
* **Which version of capa are you using?** You can get the exact version by running `capa --version` in your terminal.
|
* **Which version of capa are you using?** You can get the exact version by running `capa --version` in your terminal.
|
||||||
* **What's the name and version of the OS you're using**?
|
* **What's the name and version of the OS you're using**?
|
||||||
|
|
||||||
### Suggesting Enhancements
|
### Suggesting Enhancements
|
||||||
|
|
||||||
This section guides you through submitting an enhancement suggestion for capa, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion and find related suggestions.
|
This section guides you through submitting an enhancement suggestion for capa, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion and find related suggestions.
|
||||||
|
|
||||||
Before creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). Fill in [the template](./ISSUE_TEMPLATE/feature_request.md), including the steps that you imagine you would take if the feature you're requesting existed.
|
Before creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). Fill in [the template](./ISSUE_TEMPLATE/feature_request.md), including the steps that you imagine you would take if the feature you're requesting existed.
|
||||||
|
|
||||||
#### Before Submitting An Enhancement Suggestion
|
#### Before Submitting An Enhancement Suggestion
|
||||||
|
|
||||||
* **Determine [which repository the enhancement should be suggested in](#capa-and-its-repositories).**
|
* **Determine [which repository the enhancement should be suggested in](#capa-and-its-repositories).**
|
||||||
* **Perform a [cursory search](https://github.com/fireeye/capa/issues?q=is%3Aissue)** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
|
* **Perform a [cursory search](https://github.com/fireeye/capa/issues?q=is%3Aissue)** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
|
||||||
|
|
||||||
#### How Do I Submit A (Good) Enhancement Suggestion?
|
#### How Do I Submit A (Good) Enhancement Suggestion?
|
||||||
|
|
||||||
Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined [which repository](#capa-and-its-repositories) your enhancement suggestion is related to, create an issue on that repository and provide the following information:
|
Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined [which repository](#capa-and-its-repositories) your enhancement suggestion is related to, create an issue on that repository and provide the following information:
|
||||||
|
|
||||||
* **Use a clear and descriptive title** for the issue to identify the suggestion.
|
* **Use a clear and descriptive title** for the issue to identify the suggestion.
|
||||||
* **Provide a step-by-step description of the suggested enhancement** in as many details as possible.
|
* **Provide a step-by-step description of the suggested enhancement** in as many details as possible.
|
||||||
* **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
|
* **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
|
||||||
* **Describe the current behavior** and **explain which behavior you expected to see instead** and why.
|
* **Describe the current behavior** and **explain which behavior you expected to see instead** and why.
|
||||||
* **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the part of capa which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
|
* **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the part of capa which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
|
||||||
* **Explain why this enhancement would be useful** to most capa users and isn't something that can or should be implemented as an external tool that uses capa as a library.
|
* **Explain why this enhancement would be useful** to most capa users and isn't something that can or should be implemented as an external tool that uses capa as a library.
|
||||||
* **Specify which version of capa you're using.** You can get the exact version by running `capa --version` in your terminal.
|
* **Specify which version of capa you're using.** You can get the exact version by running `capa --version` in your terminal.
|
||||||
* **Specify the name and version of the OS you're using.**
|
* **Specify the name and version of the OS you're using.**
|
||||||
|
|
||||||
### Your First Code Contribution
|
### Your First Code Contribution
|
||||||
|
|
||||||
Unsure where to begin contributing to capa? You can start by looking through these `good-first-issue` and `rule-idea` issues:
|
Unsure where to begin contributing to capa? You can start by looking through these `good-first-issue` and `rule-idea` issues:
|
||||||
|
|
||||||
* [good-first-issue](https://github.com/fireeye/capa/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - issues which should only require a few lines of code, and a test or two.
|
* [good-first-issue](https://github.com/fireeye/capa/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - issues which should only require a few lines of code, and a test or two.
|
||||||
* [rule-idea](https://github.com/fireeye/capa-rules/issues?q=is%3Aissue+is%3Aopen+label%3A%22rule+idea%22) - issues that describe potential new rule ideas.
|
* [rule-idea](https://github.com/fireeye/capa-rules/issues?q=is%3Aissue+is%3Aopen+label%3A%22rule+idea%22) - issues that describe potential new rule ideas.
|
||||||
|
|
||||||
Both issue lists are sorted by total number of comments. While not perfect, number of comments is a reasonable proxy for impact a given change will have.
|
Both issue lists are sorted by total number of comments. While not perfect, number of comments is a reasonable proxy for impact a given change will have.
|
||||||
|
|
||||||
#### Local development
|
#### Local development
|
||||||
|
|
||||||
capa and all its resources can be developed locally.
|
capa and all its resources can be developed locally.
|
||||||
For instructions on how to do this, see the "Method 3" section of the [installation guide](https://github.com/fireeye/capa/blob/master/doc/installation.md).
|
For instructions on how to do this, see the "Method 3" section of the [installation guide](https://github.com/fireeye/capa/blob/master/doc/installation.md).
|
||||||
|
|
||||||
### Pull Requests
|
### Pull Requests
|
||||||
|
|
||||||
The process described here has several goals:
|
The process described here has several goals:
|
||||||
|
|
||||||
- Maintain capa's quality
|
- Maintain capa's quality
|
||||||
- Fix problems that are important to users
|
- Fix problems that are important to users
|
||||||
- Engage the community in working toward the best possible capa
|
- Engage the community in working toward the best possible capa
|
||||||
- Enable a sustainable system for capa's maintainers to review contributions
|
- Enable a sustainable system for capa's maintainers to review contributions
|
||||||
|
|
||||||
Please follow these steps to have your contribution considered by the maintainers:
|
Please follow these steps to have your contribution considered by the maintainers:
|
||||||
|
|
||||||
1. Follow all instructions in [the template](PULL_REQUEST_TEMPLATE.md)
|
1. Follow all instructions in [the template](PULL_REQUEST_TEMPLATE.md)
|
||||||
2. Follow the [styleguides](#styleguides)
|
2. Follow the [styleguides](#styleguides)
|
||||||
3. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing <details><summary>What if the status checks are failing? </summary>If a status check is failing, and you believe that the failure is unrelated to your change, please leave a comment on the pull request explaining why you believe the failure is unrelated. A maintainer will re-run the status check for you. If we conclude that the failure was a false positive, then we will open an issue to track that problem with our status check suite.</details>
|
3. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing <details><summary>What if the status checks are failing? </summary>If a status check is failing, and you believe that the failure is unrelated to your change, please leave a comment on the pull request explaining why you believe the failure is unrelated. A maintainer will re-run the status check for you. If we conclude that the failure was a false positive, then we will open an issue to track that problem with our status check suite.</details>
|
||||||
|
|
||||||
While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted.
|
While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted.
|
||||||
|
|
||||||
## Styleguides
|
## Styleguides
|
||||||
|
|
||||||
### Git Commit Messages
|
### Git Commit Messages
|
||||||
|
|
||||||
* Use the present tense ("Add feature" not "Added feature")
|
* Use the present tense ("Add feature" not "Added feature")
|
||||||
* Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
|
* Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
|
||||||
* Prefix the first line with the component in question ("rules: ..." or "render: ...")
|
* Prefix the first line with the component in question ("rules: ..." or "render: ...")
|
||||||
* Reference issues and pull requests liberally after the first line
|
* Reference issues and pull requests liberally after the first line
|
||||||
|
|
||||||
### Python Styleguide
|
### Python Styleguide
|
||||||
|
|
||||||
All Python code must adhere to the style guide used by capa:
|
All Python code must adhere to the style guide used by capa:
|
||||||
|
|
||||||
1. [PEP8](https://www.python.org/dev/peps/pep-0008/), with clarifications from
|
1. [PEP8](https://www.python.org/dev/peps/pep-0008/), with clarifications from
|
||||||
2. [Willi's style guide](https://docs.google.com/document/d/1iRpeg-w4DtibwytUyC_dDT7IGhNGBP25-nQfuBa-Fyk/edit?usp=sharing), formatted with
|
2. [Willi's style guide](https://docs.google.com/document/d/1iRpeg-w4DtibwytUyC_dDT7IGhNGBP25-nQfuBa-Fyk/edit?usp=sharing), formatted with
|
||||||
3. [isort](https://pypi.org/project/isort/) (with line width 120 and ordered by line length), and formatted with
|
3. [isort](https://pypi.org/project/isort/) (with line width 120 and ordered by line length), and formatted with
|
||||||
4. [black](https://github.com/psf/black) (with line width 120), and formatted with
|
4. [black](https://github.com/psf/black) (with line width 120), and formatted with
|
||||||
5. [dos2unix](https://linux.die.net/man/1/dos2unix)
|
5. [dos2unix](https://linux.die.net/man/1/dos2unix)
|
||||||
|
|
||||||
Our CI pipeline will reformat and enforce the Python styleguide.
|
Our CI pipeline will reformat and enforce the Python styleguide.
|
||||||
|
|
||||||
### Rules Styleguide
|
### Rules Styleguide
|
||||||
|
|
||||||
All (non-nursery) capa rules must:
|
All (non-nursery) capa rules must:
|
||||||
|
|
||||||
1. pass the [linter](https://github.com/fireeye/capa/blob/master/scripts/lint.py), and
|
1. pass the [linter](https://github.com/fireeye/capa/blob/master/scripts/lint.py), and
|
||||||
2. be formatted with [capafmt](https://github.com/fireeye/capa/blob/master/scripts/capafmt.py)
|
2. be formatted with [capafmt](https://github.com/fireeye/capa/blob/master/scripts/capafmt.py)
|
||||||
|
|
||||||
This ensures that all rules meet the same minimum level of quality and are structured in a consistent way.
|
This ensures that all rules meet the same minimum level of quality and are structured in a consistent way.
|
||||||
Our CI pipeline will reformat and enforce the capa rules styleguide.
|
Our CI pipeline will reformat and enforce the capa rules styleguide.
|
||||||
|
|||||||
94
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,47 +1,47 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
|
|
||||||
---
|
---
|
||||||
<!--
|
<!--
|
||||||
# Is your bug report related to capa rules (for example a false positive)?
|
# Is your bug report related to capa rules (for example a false positive)?
|
||||||
We use sybmodules to separate code, rules and test data. If your issue is related to capa rules, please report it at https://github.com/fireeye/capa-rules/issues.
|
We use submodules to separate code, rules and test data. If your issue is related to capa rules, please report it at https://github.com/fireeye/capa-rules/issues.
|
||||||
|
|
||||||
# Have you checked that your issue isn't already filed?
|
# Have you checked that your issue isn't already filed?
|
||||||
Please search if there is a similar issue at https://github.com/fireeye/capa/issues. If there is already a similar issue, please add more details there instead of opening a new one.
|
Please search if there is a similar issue at https://github.com/fireeye/capa/issues. If there is already a similar issue, please add more details there instead of opening a new one.
|
||||||
|
|
||||||
# Have you read capa's Code of Conduct?
|
# Have you read capa's Code of Conduct?
|
||||||
By filing an Issue, you are expected to comply with it, including treating everyone with respect: https://github.com/fireeye/capa/blob/master/.github/CODE_OF_CONDUCT.md
|
By filing an Issue, you are expected to comply with it, including treating everyone with respect: https://github.com/fireeye/capa/blob/master/.github/CODE_OF_CONDUCT.md
|
||||||
|
|
||||||
# Have you read capa's CONTRIBUTING guide?
|
# Have you read capa's CONTRIBUTING guide?
|
||||||
It contains helpful information about how to contribute to capa. Check https://github.com/fireeye/capa/blob/master/.github/CONTRIBUTING.md#reporting-bugs
|
It contains helpful information about how to contribute to capa. Check https://github.com/fireeye/capa/blob/master/.github/CONTRIBUTING.md#reporting-bugs
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
|
|
||||||
<!-- Description of the issue -->
|
<!-- Description of the issue -->
|
||||||
|
|
||||||
### Steps to Reproduce
|
### Steps to Reproduce
|
||||||
|
|
||||||
<!-- 1. First Step -->
|
<!-- 1. First Step -->
|
||||||
<!-- 2. Second Step -->
|
<!-- 2. Second Step -->
|
||||||
<!-- 3. and so on… -->
|
<!-- 3. and so on… -->
|
||||||
|
|
||||||
**Expected behavior:**
|
**Expected behavior:**
|
||||||
|
|
||||||
<!-- What you expect to happen -->
|
<!-- What you expect to happen -->
|
||||||
|
|
||||||
**Actual behavior:**
|
**Actual behavior:**
|
||||||
|
|
||||||
<!-- What actually happens -->
|
<!-- What actually happens -->
|
||||||
|
|
||||||
### Versions
|
### Versions
|
||||||
|
|
||||||
<!-- You can get this information from copy and pasting the output of `capa --version` from the command line.
|
<!-- You can get this information from copy and pasting the output of `capa --version` from the command line.
|
||||||
Please specify the component you're using (e.g. standalone tool or IDA Pro integration) and your Python version.
|
Please specify the component you're using (e.g. standalone tool or IDA Pro integration) and your Python version.
|
||||||
Also, please include the OS and what version of the OS you're running. -->
|
Also, please include the OS and what version of the OS you're running. -->
|
||||||
|
|
||||||
### Additional Information
|
### Additional Information
|
||||||
|
|
||||||
<!-- Any additional information, configuration or data that might be necessary to reproduce the issue. -->
|
<!-- Any additional information, configuration or data that might be necessary to reproduce the issue. -->
|
||||||
|
|
||||||
|
|||||||
70
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,35 +1,35 @@
|
|||||||
---
|
---
|
||||||
name: Feature request
|
name: Feature request
|
||||||
about: Suggest an idea for capa
|
about: Suggest an idea for capa
|
||||||
|
|
||||||
---
|
---
|
||||||
<!--
|
<!--
|
||||||
# Is your issue related to capa rules (for example an idea for a new rule)?
|
# Is your issue related to capa rules (for example an idea for a new rule)?
|
||||||
We use sybmodules to separate code, rules and test data. If your issue is related to capa rules, please report it at https://github.com/fireeye/capa-rules/issues.
|
We use submodules to separate code, rules and test data. If your issue is related to capa rules, please report it at https://github.com/fireeye/capa-rules/issues.
|
||||||
|
|
||||||
# Have you checked that your issue isn't already filed?
|
# Have you checked that your issue isn't already filed?
|
||||||
Please search if there is a similar issue at https://github.com/fireeye/capa/issues. If there is already a similar issue, please add more details there instead of opening a new one.
|
Please search if there is a similar issue at https://github.com/fireeye/capa/issues. If there is already a similar issue, please add more details there instead of opening a new one.
|
||||||
|
|
||||||
# Have you read capa's Code of Conduct?
|
# Have you read capa's Code of Conduct?
|
||||||
By filing an Issue, you are expected to comply with it, including treating everyone with respect: https://github.com/fireeye/capa/blob/master/.github/CODE_OF_CONDUCT.md
|
By filing an Issue, you are expected to comply with it, including treating everyone with respect: https://github.com/fireeye/capa/blob/master/.github/CODE_OF_CONDUCT.md
|
||||||
|
|
||||||
# Have you read capa's CONTRIBUTING guide?
|
# Have you read capa's CONTRIBUTING guide?
|
||||||
It contains helpful information about how to contribute to capa. Check https://github.com/fireeye/capa/blob/master/.github/CONTRIBUTING.md#suggesting-enhancements
|
It contains helpful information about how to contribute to capa. Check https://github.com/fireeye/capa/blob/master/.github/CONTRIBUTING.md#suggesting-enhancements
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|
||||||
<!-- One paragraph explanation of the feature. -->
|
<!-- One paragraph explanation of the feature. -->
|
||||||
|
|
||||||
### Motivation
|
### Motivation
|
||||||
|
|
||||||
<!-- Why are we doing this? What use cases does it support? What is the expected outcome? -->
|
<!-- Why are we doing this? What use cases does it support? What is the expected outcome? -->
|
||||||
|
|
||||||
### Describe alternatives you've considered
|
### Describe alternatives you've considered
|
||||||
|
|
||||||
<!-- A clear and concise description of the alternative solutions you've considered. -->
|
<!-- A clear and concise description of the alternative solutions you've considered. -->
|
||||||
|
|
||||||
## Additional context
|
## Additional context
|
||||||
|
|
||||||
<!-- Add any other context or screenshots about the feature request here. -->
|
<!-- Add any other context or screenshots about the feature request here. -->
|
||||||
|
|
||||||
|
|||||||
32
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<!--
|
||||||
|
Thank you for contributing to capa! :heart:
|
||||||
|
|
||||||
|
IMPORTANT NOTE
|
||||||
|
It's most important that you submit your improvements. So even if you don't use this complete template we look forward to collaborating!
|
||||||
|
|
||||||
|
Please read capa's CONTRIBUTING guide if you haven't done so already.
|
||||||
|
It contains helpful information about how to contribute to capa. Check https://github.com/fireeye/capa/blob/master/.github/CONTRIBUTING.md
|
||||||
|
|
||||||
|
PR template based on https://embeddedartistry.com/blog/2017/08/04/a-github-pull-request-template-for-your-projects/
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
<!-- Please describe the changes in this PR. Including your motivation and context helps us to review. -->
|
||||||
|
|
||||||
|
closes # (issue)
|
||||||
|
|
||||||
|
### Type of change
|
||||||
|
|
||||||
|
Please update the [CHANGELOG.md](/CHANGELOG.md)
|
||||||
|
|
||||||
|
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||||
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
|
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
|
- [ ] This change requires a documentation update
|
||||||
|
- [ ] I have made the corresponding changes to the documentation
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||||
|
- [ ] No new tests needed
|
||||||
154
.github/workflows/build.yml
vendored
@@ -1,77 +1,77 @@
|
|||||||
name: build
|
name: build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [edited, published]
|
types: [edited, published]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: PyInstaller for ${{ matrix.os }}
|
name: PyInstaller for ${{ matrix.os }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-16.04
|
- os: ubuntu-16.04
|
||||||
# use old linux so that the shared library versioning is more portable
|
# use old linux so that the shared library versioning is more portable
|
||||||
artifact_name: capa
|
artifact_name: capa
|
||||||
asset_name: linux
|
asset_name: linux
|
||||||
- os: windows-latest
|
- os: windows-2019
|
||||||
artifact_name: capa.exe
|
artifact_name: capa.exe
|
||||||
asset_name: windows
|
asset_name: windows
|
||||||
- os: macos-latest
|
- os: macos-10.15
|
||||||
artifact_name: capa
|
artifact_name: capa
|
||||||
asset_name: macos
|
asset_name: macos
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout capa
|
- name: Checkout capa
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.9
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.9
|
||||||
- if: matrix.os == 'ubuntu-latest'
|
- if: matrix.os == 'ubuntu-16.04'
|
||||||
run: sudo apt-get install -y libyaml-dev
|
run: sudo apt-get install -y libyaml-dev
|
||||||
- name: Install PyInstaller
|
- name: Install PyInstaller
|
||||||
run: pip install 'pyinstaller==4.2'
|
run: pip install 'pyinstaller==4.2'
|
||||||
- name: Install capa
|
- name: Install capa
|
||||||
run: pip install -e .
|
run: pip install -e .
|
||||||
- name: Build standalone executable
|
- name: Build standalone executable
|
||||||
run: pyinstaller .github/pyinstaller/pyinstaller.spec
|
run: pyinstaller .github/pyinstaller/pyinstaller.spec
|
||||||
- name: Does it run?
|
- name: Does it run?
|
||||||
run: dist/capa "tests/data/Practical Malware Analysis Lab 01-01.dll_"
|
run: dist/capa "tests/data/Practical Malware Analysis Lab 01-01.dll_"
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.asset_name }}
|
name: ${{ matrix.asset_name }}
|
||||||
path: dist/${{ matrix.artifact_name }}
|
path: dist/${{ matrix.artifact_name }}
|
||||||
|
|
||||||
zip:
|
zip:
|
||||||
name: zip ${{ matrix.asset_name }}
|
name: zip ${{ matrix.asset_name }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-20.04
|
||||||
needs: build
|
needs: build
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- asset_name: linux
|
- asset_name: linux
|
||||||
artifact_name: capa
|
artifact_name: capa
|
||||||
- asset_name: windows
|
- asset_name: windows
|
||||||
artifact_name: capa.exe
|
artifact_name: capa.exe
|
||||||
- asset_name: macos
|
- asset_name: macos
|
||||||
artifact_name: capa
|
artifact_name: capa
|
||||||
steps:
|
steps:
|
||||||
- name: Download ${{ matrix.asset_name }}
|
- name: Download ${{ matrix.asset_name }}
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.asset_name }}
|
name: ${{ matrix.asset_name }}
|
||||||
- name: Set executable flag
|
- name: Set executable flag
|
||||||
run: chmod +x ${{ matrix.artifact_name }}
|
run: chmod +x ${{ matrix.artifact_name }}
|
||||||
- name: Set zip name
|
- name: Set zip name
|
||||||
run: echo "zip_name=capa-${GITHUB_REF#refs/tags/}-${{ matrix.asset_name }}.zip" >> $GITHUB_ENV
|
run: echo "zip_name=capa-${GITHUB_REF#refs/tags/}-${{ matrix.asset_name }}.zip" >> $GITHUB_ENV
|
||||||
- name: Zip ${{ matrix.artifact_name }} into ${{ env.zip_name }}
|
- name: Zip ${{ matrix.artifact_name }} into ${{ env.zip_name }}
|
||||||
run: zip ${{ env.zip_name }} ${{ matrix.artifact_name }}
|
run: zip ${{ env.zip_name }} ${{ matrix.artifact_name }}
|
||||||
- name: Upload ${{ env.zip_name }} to GH Release
|
- name: Upload ${{ env.zip_name }} to GH Release
|
||||||
uses: svenstaro/upload-release-action@v2
|
uses: svenstaro/upload-release-action@v2
|
||||||
with:
|
with:
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN}}
|
repo_token: ${{ secrets.GITHUB_TOKEN}}
|
||||||
file: ${{ env.zip_name }}
|
file: ${{ env.zip_name }}
|
||||||
tag: ${{ github.ref }}
|
tag: ${{ github.ref }}
|
||||||
|
|||||||
58
.github/workflows/publish.yml
vendored
@@ -1,29 +1,29 @@
|
|||||||
# This workflows will upload a Python Package using Twine when a release is created
|
# This workflows will upload a Python Package using Twine when a release is created
|
||||||
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
|
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
|
||||||
|
|
||||||
name: publish to pypi
|
name: publish to pypi
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: '2.7'
|
python-version: '2.7'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install setuptools wheel twine
|
pip install setuptools wheel twine
|
||||||
- name: Build and publish
|
- name: Build and publish
|
||||||
env:
|
env:
|
||||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
python setup.py sdist bdist_wheel
|
python setup.py sdist bdist_wheel
|
||||||
twine upload --skip-existing dist/*
|
twine upload --skip-existing dist/*
|
||||||
|
|||||||
24
.github/workflows/tag.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: tag
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tag:
|
||||||
|
name: Tag capa rules
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout capa-rules
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
repository: fireeye/capa-rules
|
||||||
|
token: ${{ secrets.CAPA_TOKEN }}
|
||||||
|
- name: Tag capa-rules
|
||||||
|
run: git tag ${{ github.event.release.tag_name }}
|
||||||
|
- name: Push tag to capa-rules
|
||||||
|
uses: ad-m/github-push-action@master
|
||||||
|
with:
|
||||||
|
repository: fireeye/capa-rules
|
||||||
|
github_token: ${{ secrets.CAPA_TOKEN }}
|
||||||
|
tags: true
|
||||||
31
.github/workflows/tests.yml
vendored
@@ -8,7 +8,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
code_style:
|
code_style:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout capa
|
- name: Checkout capa
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
run: black -l 120 --check .
|
run: black -l 120 --check .
|
||||||
|
|
||||||
rule_linter:
|
rule_linter:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout capa with rules submodule
|
- name: Checkout capa with rules submodule
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -41,30 +41,39 @@ jobs:
|
|||||||
run: python scripts/lint.py rules/
|
run: python scripts/lint.py rules/
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
name: Tests in ${{ matrix.python }}
|
name: Tests in ${{ matrix.python-version }} on ${{ matrix.os }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
needs: [code_style, rule_linter]
|
needs: [code_style, rule_linter]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
os: [ubuntu-20.04, windows-2019, macos-10.15]
|
||||||
|
# across all operating systems
|
||||||
|
python-version: [3.6, 3.9]
|
||||||
include:
|
include:
|
||||||
- python: 2.7
|
# on Ubuntu run these as well
|
||||||
- python: 3.7
|
- os: ubuntu-20.04
|
||||||
- python: 3.8
|
python-version: 2.7
|
||||||
- python: 3.9.1
|
- os: ubuntu-20.04
|
||||||
|
python-version: 3.7
|
||||||
|
- os: ubuntu-20.04
|
||||||
|
python-version: 3.8
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout capa with submodules
|
- name: Checkout capa with submodules
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Set up Python ${{ matrix.python }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install pyyaml
|
- name: Install pyyaml
|
||||||
|
if: matrix.os == 'ubuntu-20.04'
|
||||||
run: sudo apt-get install -y libyaml-dev
|
run: sudo apt-get install -y libyaml-dev
|
||||||
|
- name: Install Microsoft Visual C++ 9.0
|
||||||
|
if: matrix.os == 'windows-2019' && matrix.python-version == '2.7'
|
||||||
|
run: choco install vcpython27
|
||||||
- name: Install capa
|
- name: Install capa
|
||||||
run: pip install -e .[dev]
|
run: pip install -e .[dev]
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: pytest tests/
|
run: pytest tests/
|
||||||
|
|
||||||
|
|||||||
1217
CHANGELOG.md
@@ -1,7 +1,10 @@
|
|||||||

|

|
||||||
|
|
||||||
|
[](https://pypi.org/project/flare-capa)
|
||||||
|
[](https://github.com/fireeye/capa/releases)
|
||||||
|
[](https://github.com/fireeye/capa-rules)
|
||||||
[](https://github.com/fireeye/capa/actions?query=workflow%3ACI+event%3Apush+branch%3Amaster)
|
[](https://github.com/fireeye/capa/actions?query=workflow%3ACI+event%3Apush+branch%3Amaster)
|
||||||
[](https://github.com/fireeye/capa-rules)
|
[](https://github.com/fireeye/capa/releases)
|
||||||
[](LICENSE.txt)
|
[](LICENSE.txt)
|
||||||
|
|
||||||
capa detects capabilities in executable files.
|
capa detects capabilities in executable files.
|
||||||
@@ -146,8 +149,8 @@ rule:
|
|||||||
The [github.com/fireeye/capa-rules](https://github.com/fireeye/capa-rules) repository contains hundreds of standard library rules that are distributed with capa.
|
The [github.com/fireeye/capa-rules](https://github.com/fireeye/capa-rules) repository contains hundreds of standard library rules that are distributed with capa.
|
||||||
Please learn to write rules and contribute new entries as you find interesting techniques in malware.
|
Please learn to write rules and contribute new entries as you find interesting techniques in malware.
|
||||||
|
|
||||||
If you use IDA Pro, then you can use the [capa explorer plugin](capa/ida/plugin/).
|
If you use IDA Pro, then you can use the [capa explorer](capa/ida/plugin/) plugin.
|
||||||
capa explorer lets you quickly identify and navigate to interesting areas of a program and help you build new capa rules out of the features extracted directly from your IDB.
|
capa explorer helps you identify interesting areas of a program and build new capa rules using features extracted directly from your IDA Pro database.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,20 @@ def hex_string(h):
|
|||||||
return " ".join(h[i : i + 2] for i in range(0, len(h), 2)).upper()
|
return " ".join(h[i : i + 2] for i in range(0, len(h), 2)).upper()
|
||||||
|
|
||||||
|
|
||||||
|
def escape_string(s):
|
||||||
|
"""escape special characters"""
|
||||||
|
s = repr(s)
|
||||||
|
if not s.startswith(('"', "'")):
|
||||||
|
# u'hello\r\nworld' -> hello\\r\\nworld
|
||||||
|
s = s[2:-1]
|
||||||
|
else:
|
||||||
|
# 'hello\r\nworld' -> hello\\r\\nworld
|
||||||
|
s = s[1:-1]
|
||||||
|
s = s.replace("\\'", "'") # repr() may escape "'" in some edge cases, remove
|
||||||
|
s = s.replace('"', '\\"') # repr() does not escape '"', add
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
class Feature(object):
|
class Feature(object):
|
||||||
def __init__(self, value, arch=None, description=None):
|
def __init__(self, value, arch=None, description=None):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||

|

|
||||||
|
|
||||||
capa explorer is an IDA Pro plugin written in Python that integrates the FLARE team's open-source framework, capa, with IDA. capa is a framework that uses a well-defined collection of rules to
|
capa explorer is an IDAPython plugin that integrates the FLARE team's open-source framework, capa, with IDA Pro. capa is a framework that uses a well-defined collection of rules to
|
||||||
identify capabilities in a program. You can run capa against a PE file or shellcode and it tells you what it thinks the program can do. For example, it might suggest that
|
identify capabilities in a program. You can run capa against a PE file or shellcode and it tells you what it thinks the program can do. For example, it might suggest that
|
||||||
the program is a backdoor, can install services, or relies on HTTP to communicate. You can use capa explorer to run capa directly on an IDA database without requiring access
|
the program is a backdoor, can install services, or relies on HTTP to communicate. capa explorer runs capa directly against your IDA Pro database (IDB) without requiring access
|
||||||
to or execution of the source binary. Once a database has been analyzed, capa explorer can be used to quickly identify and navigate to interesting areas of a program and manually build new capa rules out
|
to the original binary file. Once a database has been analyzed, capa explorer helps you identify interesting areas of a program and build new capa rules using features extracted from your IDB.
|
||||||
of the features extracted directly from your IDB.
|
|
||||||
|
|
||||||
We love using capa explorer during malware analysis because it teaches us what parts of a program suggest a behavior. As we click on rows, capa explorer jumps directly
|
We love using capa explorer during malware analysis because it teaches us what parts of a program suggest a behavior. As we click on rows, capa explorer jumps directly
|
||||||
to important addresses in the IDA Pro database and highlights key features in the Disassembly view so they stand out visually. To illustrate, we use capa explorer to
|
to important addresses in the IDB and highlights key features in the Disassembly view so they stand out visually. To illustrate, we use capa explorer to
|
||||||
analyze Lab 14-02 from [Practical Malware Analysis](https://nostarch.com/malware) (PMA) available [here](https://practicalmalwareanalysis.com/labs/). Our goal is to understand
|
analyze Lab 14-02 from [Practical Malware Analysis](https://nostarch.com/malware) (PMA) available [here](https://practicalmalwareanalysis.com/labs/). Our goal is to understand
|
||||||
the program's functionality.
|
the program's functionality.
|
||||||
|
|
||||||
@@ -15,16 +14,15 @@ After loading Lab 14-02 into IDA and analyzing the database with capa explorer,
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
We can use capa explorer to navigate the IDA Disassembly view directly to the suspect function and get an assembly-level breakdown of why capa matched `self delete via COMSPEC environment variable`
|
We can use capa explorer to navigate our Disassembly view directly to the suspect function and get an assembly-level breakdown of why capa matched `self delete via COMSPEC environment variable`.
|
||||||
for this particular function.
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Using the `Rule Information` and `Details` columns capa explorer shows us that the suspect function matched `self delete via COMSPEC environment variable` because it contains capa rule matches for `create process`, `get COMSPEC environment variable`,
|
Using the `Rule Information` and `Details` columns capa explorer shows us that the suspect function matched `self delete via COMSPEC environment variable` because it contains capa rule matches for `create process`, `get COMSPEC environment variable`,
|
||||||
and `query environment variable`, references to the strings `COMSPEC`, ` > nul`, and `/c del`, and calls to the Windows API functions `GetEnvironmentVariableA` and `ShellExecuteEx`.
|
and `query environment variable`, references to the strings `COMSPEC`, ` > nul`, and `/c del `, and calls to the Windows API functions `GetEnvironmentVariableA` and `ShellExecuteEx`.
|
||||||
|
|
||||||
capa explorer also helps you build new capa rules. To start select the `Rule Generator` tab, navigate to a function in the IDA `Disassembly` view,
|
capa explorer also helps you build new capa rules. To start select the `Rule Generator` tab, navigate to a function in your Disassembly view,
|
||||||
and click `Analyze`. capa explorer will extract features from this function and display them in the `Function Features` pane. You can add features listed in this pane to the `Editor` pane
|
and click `Analyze`. capa explorer will extract features from the function and display them in the `Features` pane. You can add features listed in this pane to the `Editor` pane
|
||||||
by either double-clicking a feature or using multi-select + right-click to add multiple features at once. The `Preview` and `Editor` panes help edit your rule. Use the `Preview` pane
|
by either double-clicking a feature or using multi-select + right-click to add multiple features at once. The `Preview` and `Editor` panes help edit your rule. Use the `Preview` pane
|
||||||
to modify the rule text directly and the `Editor` pane to construct and rearrange your hierarchy of statements and features. When you finish a rule you can save it directly to a file by clicking `Save`.
|
to modify the rule text directly and the `Editor` pane to construct and rearrange your hierarchy of statements and features. When you finish a rule you can save it directly to a file by clicking `Save`.
|
||||||
|
|
||||||
@@ -44,7 +42,7 @@ If you encounter issues with your specific setup, please open a new [Issue](http
|
|||||||
|
|
||||||
### Supported File Types
|
### Supported File Types
|
||||||
|
|
||||||
capa explorer is limited to the file types supported by capa, which includes:
|
capa explorer is limited to the file types supported by capa, which include:
|
||||||
|
|
||||||
* Windows 32-bit and 64-bit PE files
|
* Windows 32-bit and 64-bit PE files
|
||||||
* Windows 32-bit and 64-bit shellcode
|
* Windows 32-bit and 64-bit shellcode
|
||||||
@@ -62,50 +60,48 @@ You can install capa explorer using the following steps:
|
|||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
1. Run IDA and analyze a supported file type (select the `Manual Load` and `Load Resources` options in IDA for best results)
|
1. Open IDA and analyze a supported file type (select the `Manual Load` and `Load Resources` options in IDA for best results)
|
||||||
2. Open capa explorer in IDA by navigating to `Edit > Plugins > FLARE capa explorer` or using the keyboard shortcut `Alt+F5`
|
2. Open capa explorer in IDA by navigating to `Edit > Plugins > FLARE capa explorer` or using the keyboard shortcut `Alt+F5`
|
||||||
3. Select the `Program Analysis` tab
|
3. Select the `Program Analysis` tab
|
||||||
4. Click the `Analyze` button
|
4. Click the `Analyze` button
|
||||||
|
|
||||||
When running capa explorer for the first time you are prompted to select a file directory containing capa rules. The plugin conveniently
|
When running capa explorer for the first time you are prompted to select a file directory containing capa rules. The plugin conveniently
|
||||||
remembers your selection for future runs; you can change this selection by navigating to `Settings > Change default rules directory...`. We recommend
|
remembers your selection for future runs; you can change this selection and other default settings by clicking `Settings`. We recommend
|
||||||
downloading and using the [standard collection of capa rules](https://github.com/fireeye/capa-rules) when getting started with the plugin.
|
downloading and using the [standard collection of capa rules](https://github.com/fireeye/capa-rules) when getting started with the plugin.
|
||||||
|
|
||||||
#### Tips for Program Analysis
|
#### Tips for Program Analysis
|
||||||
|
|
||||||
* Start analysis by clicking the `Analyze` button
|
* Start analysis by clicking the `Analyze` button
|
||||||
* Reset the plugin user interface and remove highlighting from IDA disassembly view by clicking the `Reset` button
|
* Reset the plugin user interface and remove highlighting from your Disassembly view by clicking the `Reset` button
|
||||||
* Change your capa rules directory by navigating to `Settings > Change default rules directory...` from the plugin menu
|
* Change your capa rules directory and other default settings by clicking `Settings`
|
||||||
* Hover your cursor over a rule match to view the source content of the rule
|
* Hover your cursor over a rule match to view the source content of the rule
|
||||||
* Double-click the `Address` column to navigate the IDA Disassembly view to the associated feature
|
* Double-click the `Address` column to navigate your Disassembly view to the address of the associated feature
|
||||||
* Double-click a result in the `Rule Information` column to expand its children
|
* Double-click a result in the `Rule Information` column to expand its children
|
||||||
* Select a checkbox in the `Rule Information` column to highlight the address of the associated feature in the IDA Dissasembly view
|
* Select a checkbox in the `Rule Information` column to highlight the address of the associated feature in your Dissasembly view
|
||||||
|
|
||||||
#### Tips for Rule Generator
|
#### Tips for Rule Generator
|
||||||
|
|
||||||
* Navigate to a function in the `Disassembly` view and click`Analyze` to get started
|
* Navigate to a function in your Disassembly view and click`Analyze` to get started
|
||||||
* Double-click or multi-select + right-click in the `Function Features` pane to add features to the `Editor` pane
|
* Double-click or use multi-select + right-click to add features from the `Features` pane to the `Editor` pane
|
||||||
* Right-click features in the `Editor` pane to make modifications
|
* Right-click features in the `Editor` pane to make context-specific modifications
|
||||||
* Drag-and-drop (single click + multi-select support) features in the `Editor` pane to quickly build a hierarchy of statements and features
|
* Drag-and-drop (single click + multi-select support) features in the `Editor` pane to construct your hierarchy of statements and features
|
||||||
* Right-click anywhere in the `Editor` pane not on a feature to quickly remove all features
|
* Right-click anywhere in the `Editor` pane not on a feature to remove all features
|
||||||
* Add descriptions/comments by placing editing the appropriate column in the `Editor` pane
|
* Add descriptions or comments to a feature by editing the corresponding column in the `Editor` pane
|
||||||
* Directly edit rule text, including rule metadata fields using the `Preview` pane
|
* Directly edit rule text and metadata fields using the `Preview` pane
|
||||||
* Change the default rule author and default scope displayed in the `Preview` pane by navigating to `Settings`
|
* Change the default rule author and default rule scope displayed in the `Preview` pane by clicking `Settings`
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Because capa explorer is packaged with capa you will need to install capa locally for development.
|
capa explorer is packaged with capa so you will need to install capa locally for development. You can install capa locally by following the steps outlined in `Method 3: Inspecting the capa source code` of the [capa
|
||||||
|
|
||||||
You can install capa locally by following the steps outlined in `Method 3: Inspecting the capa source code` of the [capa
|
|
||||||
installation guide](https://github.com/fireeye/capa/blob/master/doc/installation.md#method-3-inspecting-the-capa-source-code). Once installed, copy [capa_explorer.py](https://raw.githubusercontent.com/fireeye/capa/master/capa/ida/plugin/capa_explorer.py)
|
installation guide](https://github.com/fireeye/capa/blob/master/doc/installation.md#method-3-inspecting-the-capa-source-code). Once installed, copy [capa_explorer.py](https://raw.githubusercontent.com/fireeye/capa/master/capa/ida/plugin/capa_explorer.py)
|
||||||
to your IDA plugins directory to run the plugin in IDA.
|
to your plugins directory to install capa explorer in IDA.
|
||||||
|
|
||||||
### Components
|
### Components
|
||||||
|
|
||||||
capa explorer consists of two main components:
|
capa explorer consists of two main components:
|
||||||
|
|
||||||
* An IDA [feature extractor](https://github.com/fireeye/capa/tree/master/capa/features/extractors/ida) built on top of IDA's binary analysis engine
|
* An [feature extractor](https://github.com/fireeye/capa/tree/master/capa/features/extractors/ida) built on top of IDA's binary analysis engine
|
||||||
* This component uses IDAPython to extract [capa features](https://github.com/fireeye/capa-rules/blob/master/doc/format.md#extracted-features) from the IDA database such as strings,
|
* This component uses IDAPython to extract [capa features](https://github.com/fireeye/capa-rules/blob/master/doc/format.md#extracted-features) from your IDBs such as strings,
|
||||||
disassembly, and control flow; these extracted features are used by capa to find feature combinations that result in a rule match
|
disassembly, and control flow; these extracted features are used by capa to find feature combinations that result in a rule match
|
||||||
* An [interactive user interface](https://github.com/fireeye/capa/tree/master/capa/ida/plugin) for displaying and exploring capa rule matches
|
* An [interactive user interface](https://github.com/fireeye/capa/tree/master/capa/ida/plugin) for displaying and exploring capa rule matches
|
||||||
* This component integrates the IDA feature extractor and capa, providing an interactive user interface to dissect rule matches found by capa using features extracted by the IDA feature extractor
|
* This component integrates the feature extractor and capa, providing an interactive user interface to dissect rule matches found by capa using features extracted directly from your IDBs
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ from capa.ida.plugin.proxy import CapaExplorerRangeProxyModel, CapaExplorerSearc
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
settings = ida_settings.IDASettings("capa")
|
settings = ida_settings.IDASettings("capa")
|
||||||
|
|
||||||
|
CAPA_SETTINGS_RULE_PATH = "rule_path"
|
||||||
|
CAPA_SETTINGS_RULEGEN_AUTHOR = "rulegen_author"
|
||||||
|
CAPA_SETTINGS_RULEGEN_SCOPE = "rulegen_scope"
|
||||||
|
|
||||||
|
|
||||||
def write_file(path, data):
|
def write_file(path, data):
|
||||||
""" """
|
""" """
|
||||||
@@ -166,6 +170,60 @@ class CapaExplorerFeatureExtractor(capa.features.extractors.ida.IdaFeatureExtrac
|
|||||||
return super(CapaExplorerFeatureExtractor, self).extract_function_features(f)
|
return super(CapaExplorerFeatureExtractor, self).extract_function_features(f)
|
||||||
|
|
||||||
|
|
||||||
|
class QLineEditClicked(QtWidgets.QLineEdit):
|
||||||
|
def __init__(self, content, parent=None):
|
||||||
|
""" """
|
||||||
|
super(QLineEditClicked, self).__init__(content, parent)
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, e):
|
||||||
|
""" """
|
||||||
|
old = self.text()
|
||||||
|
new = str(
|
||||||
|
QtWidgets.QFileDialog.getExistingDirectory(
|
||||||
|
self.parent(), "Please select a capa rules directory", settings.user.get(CAPA_SETTINGS_RULE_PATH, "")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if new:
|
||||||
|
self.setText(new)
|
||||||
|
else:
|
||||||
|
self.setText(old)
|
||||||
|
|
||||||
|
|
||||||
|
class CapaSettingsInputDialog(QtWidgets.QDialog):
|
||||||
|
def __init__(self, title, parent=None):
|
||||||
|
""" """
|
||||||
|
super(CapaSettingsInputDialog, self).__init__(parent)
|
||||||
|
|
||||||
|
self.setWindowTitle(title)
|
||||||
|
self.setMinimumWidth(500)
|
||||||
|
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
|
||||||
|
|
||||||
|
self.edit_rule_path = QLineEditClicked(settings.user.get(CAPA_SETTINGS_RULE_PATH, ""))
|
||||||
|
self.edit_rule_author = QtWidgets.QLineEdit(settings.user.get(CAPA_SETTINGS_RULEGEN_AUTHOR, ""))
|
||||||
|
self.edit_rule_scope = QtWidgets.QComboBox()
|
||||||
|
|
||||||
|
scopes = ("file", "function", "basic block")
|
||||||
|
|
||||||
|
self.edit_rule_scope.addItems(scopes)
|
||||||
|
self.edit_rule_scope.setCurrentIndex(scopes.index(settings.user.get(CAPA_SETTINGS_RULEGEN_SCOPE, "function")))
|
||||||
|
|
||||||
|
buttons = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel, self)
|
||||||
|
|
||||||
|
layout = QtWidgets.QFormLayout(self)
|
||||||
|
layout.addRow("capa rules path", self.edit_rule_path)
|
||||||
|
layout.addRow("Default rule author", self.edit_rule_author)
|
||||||
|
layout.addRow("Default rule scope", self.edit_rule_scope)
|
||||||
|
|
||||||
|
layout.addWidget(buttons)
|
||||||
|
|
||||||
|
buttons.accepted.connect(self.accept)
|
||||||
|
buttons.rejected.connect(self.reject)
|
||||||
|
|
||||||
|
def get_values(self):
|
||||||
|
""" """
|
||||||
|
return self.edit_rule_path.text(), self.edit_rule_author.text(), self.edit_rule_scope.currentText()
|
||||||
|
|
||||||
|
|
||||||
class CapaExplorerForm(idaapi.PluginForm):
|
class CapaExplorerForm(idaapi.PluginForm):
|
||||||
"""form element for plugin interface"""
|
"""form element for plugin interface"""
|
||||||
|
|
||||||
@@ -197,11 +255,11 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
self.view_rulegen = None
|
self.view_rulegen = None
|
||||||
self.view_tabs = None
|
self.view_tabs = None
|
||||||
self.view_tab_rulegen = None
|
self.view_tab_rulegen = None
|
||||||
self.view_menu_bar = None
|
|
||||||
self.view_status_label = None
|
self.view_status_label = None
|
||||||
self.view_buttons = None
|
self.view_buttons = None
|
||||||
self.view_analyze_button = None
|
self.view_analyze_button = None
|
||||||
self.view_reset_button = None
|
self.view_reset_button = None
|
||||||
|
self.view_settings_button = None
|
||||||
self.view_save_button = None
|
self.view_save_button = None
|
||||||
|
|
||||||
self.view_rulegen_preview = None
|
self.view_rulegen_preview = None
|
||||||
@@ -273,10 +331,6 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
self.load_view_status_label()
|
self.load_view_status_label()
|
||||||
self.load_view_buttons()
|
self.load_view_buttons()
|
||||||
|
|
||||||
# load menu bar and sub menus
|
|
||||||
self.load_view_menu_bar()
|
|
||||||
self.load_configure_menu()
|
|
||||||
|
|
||||||
# load parent view
|
# load parent view
|
||||||
self.load_view_parent()
|
self.load_view_parent()
|
||||||
|
|
||||||
@@ -285,11 +339,6 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
tabs = QtWidgets.QTabWidget()
|
tabs = QtWidgets.QTabWidget()
|
||||||
self.view_tabs = tabs
|
self.view_tabs = tabs
|
||||||
|
|
||||||
def load_view_menu_bar(self):
|
|
||||||
"""load menu bar"""
|
|
||||||
bar = QtWidgets.QMenuBar()
|
|
||||||
self.view_menu_bar = bar
|
|
||||||
|
|
||||||
def load_view_checkbox_limit_by(self):
|
def load_view_checkbox_limit_by(self):
|
||||||
"""load limit results by function checkbox"""
|
"""load limit results by function checkbox"""
|
||||||
check = QtWidgets.QCheckBox("Limit results to current function")
|
check = QtWidgets.QCheckBox("Limit results to current function")
|
||||||
@@ -319,19 +368,23 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
analyze_button = QtWidgets.QPushButton("Analyze")
|
analyze_button = QtWidgets.QPushButton("Analyze")
|
||||||
reset_button = QtWidgets.QPushButton("Reset")
|
reset_button = QtWidgets.QPushButton("Reset")
|
||||||
save_button = QtWidgets.QPushButton("Save")
|
save_button = QtWidgets.QPushButton("Save")
|
||||||
|
settings_button = QtWidgets.QPushButton("Settings")
|
||||||
|
|
||||||
analyze_button.clicked.connect(self.slot_analyze)
|
analyze_button.clicked.connect(self.slot_analyze)
|
||||||
reset_button.clicked.connect(self.slot_reset)
|
reset_button.clicked.connect(self.slot_reset)
|
||||||
save_button.clicked.connect(self.slot_save)
|
save_button.clicked.connect(self.slot_save)
|
||||||
|
settings_button.clicked.connect(self.slot_settings)
|
||||||
|
|
||||||
layout = QtWidgets.QHBoxLayout()
|
layout = QtWidgets.QHBoxLayout()
|
||||||
layout.addWidget(analyze_button)
|
layout.addWidget(analyze_button)
|
||||||
layout.addWidget(reset_button)
|
layout.addWidget(reset_button)
|
||||||
layout.addStretch(2)
|
layout.addWidget(settings_button)
|
||||||
|
layout.addStretch(3)
|
||||||
layout.addWidget(save_button, alignment=QtCore.Qt.AlignRight)
|
layout.addWidget(save_button, alignment=QtCore.Qt.AlignRight)
|
||||||
|
|
||||||
self.view_analyze_button = analyze_button
|
self.view_analyze_button = analyze_button
|
||||||
self.view_reset_button = reset_button
|
self.view_reset_button = reset_button
|
||||||
|
self.view_settings_button = settings_button
|
||||||
self.view_save_button = save_button
|
self.view_save_button = save_button
|
||||||
self.view_buttons = layout
|
self.view_buttons = layout
|
||||||
|
|
||||||
@@ -350,7 +403,6 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
layout.addWidget(self.view_tabs)
|
layout.addWidget(self.view_tabs)
|
||||||
layout.addLayout(self.view_buttons)
|
layout.addLayout(self.view_buttons)
|
||||||
layout.addWidget(self.view_status_label)
|
layout.addWidget(self.view_status_label)
|
||||||
layout.setMenuBar(self.view_menu_bar)
|
|
||||||
|
|
||||||
self.parent.setLayout(layout)
|
self.parent.setLayout(layout)
|
||||||
|
|
||||||
@@ -450,27 +502,6 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
|
|
||||||
self.view_tabs.addTab(tab, "Rule Generator")
|
self.view_tabs.addTab(tab, "Rule Generator")
|
||||||
|
|
||||||
def load_configure_menu(self):
|
|
||||||
""" """
|
|
||||||
actions = (
|
|
||||||
("Change default rules directory...", "Set default rules directory", self.slot_change_rules_dir),
|
|
||||||
("Change default rule author...", "Set default rule author", self.slot_change_rule_author),
|
|
||||||
("Change default rule scope...", "Set default rule scope", self.slot_change_rule_scope),
|
|
||||||
)
|
|
||||||
self.load_menu("Settings", actions)
|
|
||||||
|
|
||||||
def load_menu(self, title, actions):
|
|
||||||
"""load menu actions
|
|
||||||
|
|
||||||
@param title: menu name displayed in UI
|
|
||||||
@param actions: tuple of tuples containing action name, tooltip, and slot function
|
|
||||||
"""
|
|
||||||
menu = self.view_menu_bar.addMenu(title)
|
|
||||||
for (name, _, slot) in actions:
|
|
||||||
action = QtWidgets.QAction(name, self.parent)
|
|
||||||
action.triggered.connect(slot)
|
|
||||||
menu.addAction(action)
|
|
||||||
|
|
||||||
def load_ida_hooks(self):
|
def load_ida_hooks(self):
|
||||||
"""load IDA UI hooks"""
|
"""load IDA UI hooks"""
|
||||||
# map named action (defined in idagui.cfg) to Python function
|
# map named action (defined in idagui.cfg) to Python function
|
||||||
@@ -567,7 +598,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# resolve rules directory - check self and settings first, then ask user
|
# resolve rules directory - check self and settings first, then ask user
|
||||||
if not os.path.exists(settings.user.get("rule_path", "")):
|
if not os.path.exists(settings.user.get(CAPA_SETTINGS_RULE_PATH, "")):
|
||||||
idaapi.info("Please select a file directory containing capa rules.")
|
idaapi.info("Please select a file directory containing capa rules.")
|
||||||
path = self.ask_user_directory()
|
path = self.ask_user_directory()
|
||||||
if not path:
|
if not path:
|
||||||
@@ -575,7 +606,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
"You must select a file directory containing capa rules before analysis can be run. The standard collection of capa rules can be downloaded from https://github.com/fireeye/capa-rules."
|
"You must select a file directory containing capa rules before analysis can be run. The standard collection of capa rules can be downloaded from https://github.com/fireeye/capa-rules."
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
settings.user["rule_path"] = path
|
settings.user[CAPA_SETTINGS_RULE_PATH] = path
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to load capa rules (error: %s).", e)
|
logger.error("Failed to load capa rules (error: %s).", e)
|
||||||
return False
|
return False
|
||||||
@@ -584,8 +615,9 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
logger.info("User cancelled analysis.")
|
logger.info("User cancelled analysis.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
rule_path = settings.user["rule_path"]
|
rule_path = settings.user[CAPA_SETTINGS_RULE_PATH]
|
||||||
try:
|
try:
|
||||||
|
# TODO refactor: this first part is identical to capa.main.get_rules
|
||||||
if not os.path.exists(rule_path):
|
if not os.path.exists(rule_path):
|
||||||
raise IOError("rule path %s does not exist or cannot be accessed" % rule_path)
|
raise IOError("rule path %s does not exist or cannot be accessed" % rule_path)
|
||||||
|
|
||||||
@@ -601,8 +633,8 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
continue
|
continue
|
||||||
for file in files:
|
for file in files:
|
||||||
if not file.endswith(".yml"):
|
if not file.endswith(".yml"):
|
||||||
if not (file.endswith(".md") or file.endswith(".git") or file.endswith(".txt")):
|
if not (file.startswith(".git") or file.endswith((".git", ".md", ".txt"))):
|
||||||
# expect to see readme.md, format.md, and maybe a .git directory
|
# expect to see .git* files, readme.md, format.md, and maybe a .git directory
|
||||||
# other things maybe are rules, but are mis-named.
|
# other things maybe are rules, but are mis-named.
|
||||||
logger.warning("skipping non-.yml file: %s", file)
|
logger.warning("skipping non-.yml file: %s", file)
|
||||||
continue
|
continue
|
||||||
@@ -613,7 +645,8 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
total_paths = len(rule_paths)
|
total_paths = len(rule_paths)
|
||||||
for (i, rule_path) in enumerate(rule_paths):
|
for (i, rule_path) in enumerate(rule_paths):
|
||||||
update_wait_box(
|
update_wait_box(
|
||||||
"loading capa rules from %s (%d of %d)" % (settings.user["rule_path"], i + 1, total_paths)
|
"loading capa rules from %s (%d of %d)"
|
||||||
|
% (settings.user[CAPA_SETTINGS_RULE_PATH], i + 1, total_paths)
|
||||||
)
|
)
|
||||||
if ida_kernwin.user_cancelled():
|
if ida_kernwin.user_cancelled():
|
||||||
raise UserCancelledError("user cancelled")
|
raise UserCancelledError("user cancelled")
|
||||||
@@ -632,12 +665,14 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
logger.info("User cancelled analysis.")
|
logger.info("User cancelled analysis.")
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
capa.ida.helpers.inform_user_ida_ui("Failed to load capa rules from %s" % settings.user["rule_path"])
|
capa.ida.helpers.inform_user_ida_ui(
|
||||||
logger.error("Failed to load rules from %s (error: %s).", settings.user["rule_path"], e)
|
"Failed to load capa rules from %s" % settings.user[CAPA_SETTINGS_RULE_PATH]
|
||||||
|
)
|
||||||
|
logger.error("Failed to load rules from %s (error: %s).", settings.user[CAPA_SETTINGS_RULE_PATH], e)
|
||||||
logger.error(
|
logger.error(
|
||||||
"Make sure your file directory contains properly formatted capa rules. You can download the standard collection of capa rules from https://github.com/fireeye/capa-rules."
|
"Make sure your file directory contains properly formatted capa rules. You can download the standard collection of capa rules from https://github.com/fireeye/capa-rules."
|
||||||
)
|
)
|
||||||
settings.user["rule_path"] = ""
|
settings.user[CAPA_SETTINGS_RULE_PATH] = ""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.ruleset_cache = ruleset
|
self.ruleset_cache = ruleset
|
||||||
@@ -743,7 +778,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
try:
|
try:
|
||||||
self.model_data.render_capa_doc(self.doc, self.view_show_results_by_function.isChecked())
|
self.model_data.render_capa_doc(self.doc, self.view_show_results_by_function.isChecked())
|
||||||
self.set_view_status_label(
|
self.set_view_status_label(
|
||||||
"capa rules directory: %s (%d rules)" % (settings.user["rule_path"], len(self.rules_cache))
|
"capa rules directory: %s (%d rules)" % (settings.user[CAPA_SETTINGS_RULE_PATH], len(self.rules_cache))
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to render results (error: %s)", e)
|
logger.error("Failed to render results (error: %s)", e)
|
||||||
@@ -881,14 +916,14 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
# load preview and feature tree
|
# load preview and feature tree
|
||||||
self.view_rulegen_preview.load_preview_meta(
|
self.view_rulegen_preview.load_preview_meta(
|
||||||
f.start_ea if f else None,
|
f.start_ea if f else None,
|
||||||
settings.user.get("rulegen_author", "<insert_author>"),
|
settings.user.get(CAPA_SETTINGS_RULEGEN_AUTHOR, "<insert_author>"),
|
||||||
settings.user.get("rulegen_scope", "function"),
|
settings.user.get(CAPA_SETTINGS_RULEGEN_SCOPE, "function"),
|
||||||
)
|
)
|
||||||
self.view_rulegen_features.load_features(file_features, func_features)
|
self.view_rulegen_features.load_features(file_features, func_features)
|
||||||
|
|
||||||
# self.view_rulegen_header_label.setText("Function Features (%s)" % trim_function_name(f))
|
# self.view_rulegen_header_label.setText("Function Features (%s)" % trim_function_name(f))
|
||||||
self.set_view_status_label(
|
self.set_view_status_label(
|
||||||
"capa rules directory: %s (%d rules)" % (settings.user["rule_path"], len(self.rules_cache))
|
"capa rules directory: %s (%d rules)" % (settings.user[CAPA_SETTINGS_RULE_PATH], len(self.rules_cache))
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to render views (error: %s)" % e)
|
logger.error("Failed to render views (error: %s)" % e)
|
||||||
@@ -985,6 +1020,12 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
|
|
||||||
# create deep copy of current rules, add our new rule
|
# create deep copy of current rules, add our new rule
|
||||||
rules = copy.copy(self.rules_cache)
|
rules = copy.copy(self.rules_cache)
|
||||||
|
|
||||||
|
# ensure subscope rules are included
|
||||||
|
for sub in rule.extract_subscope_rules():
|
||||||
|
rules.append(sub)
|
||||||
|
|
||||||
|
# include our new rule in the list
|
||||||
rules.append(rule)
|
rules.append(rule)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -1066,6 +1107,16 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
elif self.view_tabs.currentIndex() == 1:
|
elif self.view_tabs.currentIndex() == 1:
|
||||||
self.save_function_analysis()
|
self.save_function_analysis()
|
||||||
|
|
||||||
|
def slot_settings(self):
|
||||||
|
""" """
|
||||||
|
dialog = CapaSettingsInputDialog("capa explorer settings", parent=self.parent)
|
||||||
|
if dialog.exec_():
|
||||||
|
(
|
||||||
|
settings.user[CAPA_SETTINGS_RULE_PATH],
|
||||||
|
settings.user[CAPA_SETTINGS_RULEGEN_AUTHOR],
|
||||||
|
settings.user[CAPA_SETTINGS_RULEGEN_SCOPE],
|
||||||
|
) = dialog.get_values()
|
||||||
|
|
||||||
def save_program_analysis(self):
|
def save_program_analysis(self):
|
||||||
""" """
|
""" """
|
||||||
if not self.doc:
|
if not self.doc:
|
||||||
@@ -1143,42 +1194,16 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
"""create Qt dialog to ask user for a directory"""
|
"""create Qt dialog to ask user for a directory"""
|
||||||
return str(
|
return str(
|
||||||
QtWidgets.QFileDialog.getExistingDirectory(
|
QtWidgets.QFileDialog.getExistingDirectory(
|
||||||
self.parent, "Please select a capa rules directory", settings.user["rule_path"]
|
self.parent, "Please select a capa rules directory", settings.user.get(CAPA_SETTINGS_RULE_PATH, "")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def ask_user_capa_rule_file(self):
|
def ask_user_capa_rule_file(self):
|
||||||
""" """
|
""" """
|
||||||
return QtWidgets.QFileDialog.getSaveFileName(
|
return QtWidgets.QFileDialog.getSaveFileName(
|
||||||
None, "Please select a capa rule to edit", settings.user["rule_path"], "*.yml"
|
None, "Please select a capa rule to edit", settings.user.get(CAPA_SETTINGS_RULE_PATH, ""), "*.yml"
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
def slot_change_rule_scope(self):
|
|
||||||
""" """
|
|
||||||
scope = idaapi.ask_str(str(settings.user.get("rulegen_scope", "function")), 0, "Enter default rule scope")
|
|
||||||
if scope:
|
|
||||||
settings.user["rulegen_scope"] = scope
|
|
||||||
idaapi.info("Run analysis again for your changes to take effect.")
|
|
||||||
|
|
||||||
def slot_change_rule_author(self):
|
|
||||||
""" """
|
|
||||||
author = idaapi.ask_str(str(settings.user.get("rulegen_author", "")), 0, "Enter default rule author")
|
|
||||||
if author:
|
|
||||||
settings.user["rulegen_author"] = author
|
|
||||||
idaapi.info("Run analysis again for your changes to take effect.")
|
|
||||||
|
|
||||||
def slot_change_rules_dir(self):
|
|
||||||
"""allow user to change rules directory
|
|
||||||
|
|
||||||
user selection stored in settings for future runs
|
|
||||||
"""
|
|
||||||
path = self.ask_user_directory()
|
|
||||||
if path:
|
|
||||||
settings.user["rule_path"] = path
|
|
||||||
self.rules_cache = None
|
|
||||||
self.ruleset_cache = None
|
|
||||||
idaapi.info("Run analysis again for your changes to take effect.")
|
|
||||||
|
|
||||||
def set_view_status_label(self, text):
|
def set_view_status_label(self, text):
|
||||||
"""update status label control
|
"""update status label control
|
||||||
|
|
||||||
|
|||||||
@@ -488,13 +488,17 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
|||||||
|
|
||||||
@param feature: capa feature read from doc
|
@param feature: capa feature read from doc
|
||||||
"""
|
"""
|
||||||
if feature[feature["type"]]:
|
key = feature["type"]
|
||||||
|
value = feature[feature["type"]]
|
||||||
|
if value:
|
||||||
|
if key == "string":
|
||||||
|
value = '"%s"' % capa.features.escape_string(value)
|
||||||
if feature.get("description", ""):
|
if feature.get("description", ""):
|
||||||
return "%s(%s = %s)" % (feature["type"], feature[feature["type"]], feature["description"])
|
return "%s(%s = %s)" % (key, value, feature["description"])
|
||||||
else:
|
else:
|
||||||
return "%s(%s)" % (feature["type"], feature[feature["type"]])
|
return "%s(%s)" % (key, value)
|
||||||
else:
|
else:
|
||||||
return "%s" % feature["type"]
|
return "%s" % key
|
||||||
|
|
||||||
def render_capa_doc_feature_node(self, parent, feature, locations, doc):
|
def render_capa_doc_feature_node(self, parent, feature, locations, doc):
|
||||||
"""process capa doc feature node
|
"""process capa doc feature node
|
||||||
@@ -551,7 +555,9 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if feature["type"] == "regex":
|
if feature["type"] == "regex":
|
||||||
return CapaExplorerStringViewItem(parent, display, location, feature["match"])
|
return CapaExplorerStringViewItem(
|
||||||
|
parent, display, location, '"%s"' % capa.features.escape_string(feature["match"])
|
||||||
|
)
|
||||||
|
|
||||||
if feature["type"] == "basicblock":
|
if feature["type"] == "basicblock":
|
||||||
return CapaExplorerBlockItem(parent, location)
|
return CapaExplorerBlockItem(parent, location)
|
||||||
@@ -576,7 +582,9 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
|||||||
|
|
||||||
if feature["type"] in ("string",):
|
if feature["type"] in ("string",):
|
||||||
# display string preview
|
# display string preview
|
||||||
return CapaExplorerStringViewItem(parent, display, location, feature[feature["type"]])
|
return CapaExplorerStringViewItem(
|
||||||
|
parent, display, location, '"%s"' % capa.features.escape_string(feature[feature["type"]])
|
||||||
|
)
|
||||||
|
|
||||||
if feature["type"] in ("import", "export"):
|
if feature["type"] in ("import", "export"):
|
||||||
# display no preview
|
# display no preview
|
||||||
|
|||||||
@@ -178,6 +178,9 @@ def build_context_menu(o, actions):
|
|||||||
|
|
||||||
|
|
||||||
class CapaExplorerRulgenPreview(QtWidgets.QTextEdit):
|
class CapaExplorerRulgenPreview(QtWidgets.QTextEdit):
|
||||||
|
|
||||||
|
INDENT = " " * 2
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
""" """
|
""" """
|
||||||
super(CapaExplorerRulgenPreview, self).__init__(parent)
|
super(CapaExplorerRulgenPreview, self).__init__(parent)
|
||||||
@@ -210,12 +213,99 @@ class CapaExplorerRulgenPreview(QtWidgets.QTextEdit):
|
|||||||
self.setText("\n".join(metadata_default))
|
self.setText("\n".join(metadata_default))
|
||||||
|
|
||||||
def keyPressEvent(self, e):
|
def keyPressEvent(self, e):
|
||||||
""" """
|
"""intercept key press events"""
|
||||||
if e.key() == QtCore.Qt.Key_Tab:
|
if e.key() in (QtCore.Qt.Key_Tab, QtCore.Qt.Key_Backtab):
|
||||||
self.insertPlainText(" " * 2)
|
# apparently it's not easy to implement tabs as spaces, or multi-line tab or SHIFT + Tab
|
||||||
|
# so we need to implement it ourselves so we can retain properly formatted capa rules
|
||||||
|
# when a user uses the Tab key
|
||||||
|
if self.textCursor().selection().isEmpty():
|
||||||
|
# single line, only worry about Tab
|
||||||
|
if e.key() == QtCore.Qt.Key_Tab:
|
||||||
|
self.insertPlainText(self.INDENT)
|
||||||
|
else:
|
||||||
|
# multi-line tab or SHIFT + Tab
|
||||||
|
cur = self.textCursor()
|
||||||
|
select_start_ppos = cur.selectionStart()
|
||||||
|
select_end_ppos = cur.selectionEnd()
|
||||||
|
|
||||||
|
scroll_ppos = self.verticalScrollBar().sliderPosition()
|
||||||
|
|
||||||
|
# determine lineno for first selected line, and column
|
||||||
|
cur.setPosition(select_start_ppos)
|
||||||
|
start_lineno = self.count_previous_lines_from_block(cur.block())
|
||||||
|
start_lineco = cur.columnNumber()
|
||||||
|
|
||||||
|
# determine lineno for last selected line
|
||||||
|
cur.setPosition(select_end_ppos)
|
||||||
|
end_lineno = self.count_previous_lines_from_block(cur.block())
|
||||||
|
|
||||||
|
# now we need to indent or dedent the selected lines. for now, we read the text, modify
|
||||||
|
# the lines between start_lineno and end_lineno accordingly, and then reset the view
|
||||||
|
# this might not be the best solution, but it avoids messing around with cursor positions
|
||||||
|
# to determine the beginning of lines
|
||||||
|
|
||||||
|
plain = self.toPlainText().splitlines()
|
||||||
|
|
||||||
|
if e.key() == QtCore.Qt.Key_Tab:
|
||||||
|
# user Tab, indent selected lines
|
||||||
|
lines_modified = end_lineno - start_lineno
|
||||||
|
first_modified = True
|
||||||
|
change = [self.INDENT + line for line in plain[start_lineno : end_lineno + 1]]
|
||||||
|
else:
|
||||||
|
# user SHIFT + Tab, dedent selected lines
|
||||||
|
lines_modified = 0
|
||||||
|
first_modified = False
|
||||||
|
change = []
|
||||||
|
for (lineno, line) in enumerate(plain[start_lineno : end_lineno + 1]):
|
||||||
|
if line.startswith(self.INDENT):
|
||||||
|
if lineno == 0:
|
||||||
|
# keep track if first line is modified, so we can properly display
|
||||||
|
# the text selection later
|
||||||
|
first_modified = True
|
||||||
|
lines_modified += 1
|
||||||
|
line = line[len(self.INDENT) :]
|
||||||
|
change.append(line)
|
||||||
|
|
||||||
|
# apply modifications, and reset view
|
||||||
|
plain[start_lineno : end_lineno + 1] = change
|
||||||
|
self.setPlainText("\n".join(plain) + "\n")
|
||||||
|
|
||||||
|
# now we need to properly adjust the selection positions, so users don't have to
|
||||||
|
# re-select when indenting or dedenting the same lines repeatedly
|
||||||
|
if e.key() == QtCore.Qt.Key_Tab:
|
||||||
|
# user Tab, increase increment selection positions
|
||||||
|
select_start_ppos += len(self.INDENT)
|
||||||
|
select_end_ppos += (lines_modified * len(self.INDENT)) + len(self.INDENT)
|
||||||
|
elif lines_modified:
|
||||||
|
# user SHIFT + Tab, decrease selection positions
|
||||||
|
if start_lineco not in (0, 1) and first_modified:
|
||||||
|
# only decrease start position if not in first column
|
||||||
|
select_start_ppos -= len(self.INDENT)
|
||||||
|
select_end_ppos -= lines_modified * len(self.INDENT)
|
||||||
|
|
||||||
|
# apply updated selection and restore previous scroll position
|
||||||
|
self.set_selection(select_start_ppos, select_end_ppos, len(self.toPlainText()))
|
||||||
|
self.verticalScrollBar().setSliderPosition(scroll_ppos)
|
||||||
else:
|
else:
|
||||||
super(CapaExplorerRulgenPreview, self).keyPressEvent(e)
|
super(CapaExplorerRulgenPreview, self).keyPressEvent(e)
|
||||||
|
|
||||||
|
def count_previous_lines_from_block(self, block):
|
||||||
|
"""calculate number of lines preceding block"""
|
||||||
|
count = 0
|
||||||
|
while True:
|
||||||
|
block = block.previous()
|
||||||
|
if not block.isValid():
|
||||||
|
break
|
||||||
|
count += block.lineCount()
|
||||||
|
return count
|
||||||
|
|
||||||
|
def set_selection(self, start, end, max):
|
||||||
|
"""set text selection"""
|
||||||
|
cursor = self.textCursor()
|
||||||
|
cursor.setPosition(start)
|
||||||
|
cursor.setPosition(end if end < max else max, QtGui.QTextCursor.KeepAnchor)
|
||||||
|
self.setTextCursor(cursor)
|
||||||
|
|
||||||
|
|
||||||
class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
|
class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
|
||||||
|
|
||||||
@@ -325,6 +415,11 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
|
|||||||
# create a new parent under root node, by default; new node added last position in tree
|
# create a new parent under root node, by default; new node added last position in tree
|
||||||
new_parent = self.new_expression_node(self.root, (action.data()[0], ""))
|
new_parent = self.new_expression_node(self.root, (action.data()[0], ""))
|
||||||
|
|
||||||
|
if "basic block" in action.data()[0]:
|
||||||
|
# add default child expression when nesting under basic block
|
||||||
|
new_parent.setExpanded(True)
|
||||||
|
new_parent = self.new_expression_node(new_parent, ("- or:", ""))
|
||||||
|
|
||||||
for o in self.get_features(selected=True):
|
for o in self.get_features(selected=True):
|
||||||
# take child from its parent by index, add to new parent
|
# take child from its parent by index, add to new parent
|
||||||
new_parent.addChild(o.parent().takeChild(o.parent().indexOfChild(o)))
|
new_parent.addChild(o.parent().takeChild(o.parent().indexOfChild(o)))
|
||||||
@@ -335,6 +430,15 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
|
|||||||
def slot_edit_expression(self, action):
|
def slot_edit_expression(self, action):
|
||||||
""" """
|
""" """
|
||||||
expression, o = action.data()
|
expression, o = action.data()
|
||||||
|
if "basic block" in expression and "basic block" not in o.text(
|
||||||
|
CapaExplorerRulgenEditor.get_column_feature_index()
|
||||||
|
):
|
||||||
|
# current expression is "basic block", and not changing to "basic block" expression
|
||||||
|
children = o.takeChildren()
|
||||||
|
new_parent = self.new_expression_node(o, ("- or:", ""))
|
||||||
|
for child in children:
|
||||||
|
new_parent.addChild(child)
|
||||||
|
new_parent.setExpanded(True)
|
||||||
o.setText(CapaExplorerRulgenEditor.get_column_feature_index(), expression)
|
o.setText(CapaExplorerRulgenEditor.get_column_feature_index(), expression)
|
||||||
|
|
||||||
def slot_clear_all(self, action):
|
def slot_clear_all(self, action):
|
||||||
@@ -520,11 +624,23 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
|
|||||||
|
|
||||||
# single features
|
# single features
|
||||||
for (k, v) in filter(lambda t: t[1] == 1, counted):
|
for (k, v) in filter(lambda t: t[1] == 1, counted):
|
||||||
self.new_feature_node(self.root, ("- %s: %s" % (k.name.lower(), k.get_value_str()), ""))
|
if isinstance(k, (capa.features.String,)):
|
||||||
|
value = '"%s"' % capa.features.escape_string(k.get_value_str())
|
||||||
|
else:
|
||||||
|
value = k.get_value_str()
|
||||||
|
self.new_feature_node(self.root, ("- %s: %s" % (k.name.lower(), value), ""))
|
||||||
|
|
||||||
# n > 1 features
|
# n > 1 features
|
||||||
for (k, v) in filter(lambda t: t[1] > 1, counted):
|
for (k, v) in filter(lambda t: t[1] > 1, counted):
|
||||||
self.new_feature_node(self.root, ("- count(%s): %d" % (str(k), v), ""))
|
if k.value:
|
||||||
|
if isinstance(k, (capa.features.String,)):
|
||||||
|
value = '"%s"' % capa.features.escape_string(k.get_value_str())
|
||||||
|
else:
|
||||||
|
value = k.get_value_str()
|
||||||
|
display = "- count(%s(%s)): %d" % (k.name.lower(), value, v)
|
||||||
|
else:
|
||||||
|
display = "- count(%s): %d" % (k.name.lower(), v)
|
||||||
|
self.new_feature_node(self.root, (display, ""))
|
||||||
|
|
||||||
self.expandAll()
|
self.expandAll()
|
||||||
self.update_preview()
|
self.update_preview()
|
||||||
@@ -699,9 +815,11 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
|
|||||||
if text:
|
if text:
|
||||||
for o in iterate_tree(self):
|
for o in iterate_tree(self):
|
||||||
data = o.data(0, 0x100)
|
data = o.data(0, 0x100)
|
||||||
if data and text.lower() not in data.get_value_str().lower():
|
if data:
|
||||||
o.setHidden(True)
|
to_match = data.get_value_str()
|
||||||
continue
|
if not to_match or text.lower() not in to_match.lower():
|
||||||
|
o.setHidden(True)
|
||||||
|
continue
|
||||||
o.setHidden(False)
|
o.setHidden(False)
|
||||||
o.setExpanded(True)
|
o.setExpanded(True)
|
||||||
else:
|
else:
|
||||||
@@ -776,16 +894,20 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
|
|||||||
def format_address(e):
|
def format_address(e):
|
||||||
return "%X" % e if e else ""
|
return "%X" % e if e else ""
|
||||||
|
|
||||||
|
def format_feature(feature):
|
||||||
|
""" """
|
||||||
|
name = feature.name.lower()
|
||||||
|
value = feature.get_value_str()
|
||||||
|
if isinstance(feature, (capa.features.String,)):
|
||||||
|
value = '"%s"' % capa.features.escape_string(value)
|
||||||
|
return "%s(%s)" % (name, value)
|
||||||
|
|
||||||
for (feature, eas) in sorted(features.items(), key=lambda k: sorted(k[1])):
|
for (feature, eas) in sorted(features.items(), key=lambda k: sorted(k[1])):
|
||||||
if isinstance(feature, capa.features.basicblock.BasicBlock):
|
if isinstance(feature, capa.features.basicblock.BasicBlock):
|
||||||
# filter basic blocks for now, we may want to add these back in some time
|
# filter basic blocks for now, we may want to add these back in some time
|
||||||
# in the future
|
# in the future
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if isinstance(feature, capa.features.String):
|
|
||||||
# strip string for display
|
|
||||||
feature.value = feature.value.strip()
|
|
||||||
|
|
||||||
# level 0
|
# level 0
|
||||||
if type(feature) not in self.parent_items:
|
if type(feature) not in self.parent_items:
|
||||||
self.parent_items[type(feature)] = self.new_parent_node(parent, (feature.name.lower(),))
|
self.parent_items[type(feature)] = self.new_parent_node(parent, (feature.name.lower(),))
|
||||||
@@ -794,20 +916,22 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
|
|||||||
if feature not in self.parent_items:
|
if feature not in self.parent_items:
|
||||||
if len(eas) > 1:
|
if len(eas) > 1:
|
||||||
self.parent_items[feature] = self.new_parent_node(
|
self.parent_items[feature] = self.new_parent_node(
|
||||||
self.parent_items[type(feature)], (str(feature),), feature=feature
|
self.parent_items[type(feature)], (format_feature(feature),), feature=feature
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.parent_items[feature] = self.new_leaf_node(
|
self.parent_items[feature] = self.new_leaf_node(
|
||||||
self.parent_items[type(feature)], (str(feature),), feature=feature
|
self.parent_items[type(feature)], (format_feature(feature),), feature=feature
|
||||||
)
|
)
|
||||||
|
|
||||||
# level n > 1
|
# level n > 1
|
||||||
if len(eas) > 1:
|
if len(eas) > 1:
|
||||||
for ea in sorted(eas):
|
for ea in sorted(eas):
|
||||||
self.new_leaf_node(self.parent_items[feature], (str(feature), format_address(ea)), feature=feature)
|
self.new_leaf_node(
|
||||||
|
self.parent_items[feature], (format_feature(feature), format_address(ea)), feature=feature
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
ea = eas.pop()
|
ea = eas.pop()
|
||||||
for (i, v) in enumerate((str(feature), format_address(ea))):
|
for (i, v) in enumerate((format_feature(feature), format_address(ea))):
|
||||||
self.parent_items[feature].setText(i, v)
|
self.parent_items[feature].setText(i, v)
|
||||||
self.parent_items[feature].setData(0, 0x100, feature)
|
self.parent_items[feature].setData(0, 0x100, feature)
|
||||||
|
|
||||||
|
|||||||
@@ -379,8 +379,8 @@ def get_rules(rule_path, disable_progress=False):
|
|||||||
|
|
||||||
for file in files:
|
for file in files:
|
||||||
if not file.endswith(".yml"):
|
if not file.endswith(".yml"):
|
||||||
if not (file.endswith(".md") or file.endswith(".git") or file.endswith(".txt")):
|
if not (file.startswith(".git") or file.endswith((".git", ".md", ".txt"))):
|
||||||
# expect to see readme.md, format.md, and maybe a .git directory
|
# expect to see .git* files, readme.md, format.md, and maybe a .git directory
|
||||||
# other things maybe are rules, but are mis-named.
|
# other things maybe are rules, but are mis-named.
|
||||||
logger.warning("skipping non-.yml file: %s", file)
|
logger.warning("skipping non-.yml file: %s", file)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -56,7 +56,11 @@ def render_statement(ostream, match, statement, indent=0):
|
|||||||
child = statement["child"]
|
child = statement["child"]
|
||||||
|
|
||||||
if child[child["type"]]:
|
if child[child["type"]]:
|
||||||
value = rutils.bold2(child[child["type"]])
|
if child["type"] == "string":
|
||||||
|
value = '"%s"' % capa.features.escape_string(child[child["type"]])
|
||||||
|
else:
|
||||||
|
value = child[child["type"]]
|
||||||
|
value = rutils.bold2(value)
|
||||||
if child.get("description"):
|
if child.get("description"):
|
||||||
ostream.write("count(%s(%s = %s)): " % (child["type"], value, child["description"]))
|
ostream.write("count(%s(%s = %s)): " % (child["type"], value, child["description"]))
|
||||||
else:
|
else:
|
||||||
@@ -90,6 +94,9 @@ def render_feature(ostream, match, feature, indent=0):
|
|||||||
key = "string" # render string for regex to mirror the rule source
|
key = "string" # render string for regex to mirror the rule source
|
||||||
value = feature["match"] # the match provides more information than the value for regex
|
value = feature["match"] # the match provides more information than the value for regex
|
||||||
|
|
||||||
|
if key == "string":
|
||||||
|
value = '"%s"' % capa.features.escape_string(value)
|
||||||
|
|
||||||
ostream.write(key)
|
ostream.write(key)
|
||||||
ostream.write(": ")
|
ostream.write(": ")
|
||||||
|
|
||||||
|
|||||||
@@ -736,6 +736,8 @@ class Rule(object):
|
|||||||
# the below regex makes these adjustments and while ugly, we don't have to explore the ruamel.yaml insides
|
# the below regex makes these adjustments and while ugly, we don't have to explore the ruamel.yaml insides
|
||||||
doc = re.sub(r"!!int '0x-([0-9a-fA-F]+)'", r"-0x\1", doc)
|
doc = re.sub(r"!!int '0x-([0-9a-fA-F]+)'", r"-0x\1", doc)
|
||||||
|
|
||||||
|
# normalize CRLF to LF
|
||||||
|
doc = doc.replace("\r\n", "\n")
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "1.6.0"
|
__version__ = "1.6.1"
|
||||||
|
|||||||
BIN
doc/img/changelog/tab.gif
Normal file
|
After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 320 KiB After Width: | Height: | Size: 322 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 135 KiB |
44
doc/release.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Release checklist
|
||||||
|
|
||||||
|
- [ ] Ensure all [milestoned issues/PRs](https://github.com/fireeye/capa/milestones) are addressed, or reassign to a new milestone.
|
||||||
|
- [ ] Add the `dont merge` label to all PRs that are close to be ready to merge (or merge them if they are ready) in [capa](https://github.com/fireeye/capa/pulls) and [capa-rules](https://github.com/fireeye/capa-rules/pulls).
|
||||||
|
- [ ] Ensure the [CI workflow succeeds in master](https://github.com/fireeye/capa/actions/workflows/tests.yml?query=branch%3Amaster).
|
||||||
|
- [ ] Ensure that `python scripts/lint.py rules/ --thorough` succeeds (only `missing examples` offenses are allowed in the nursery).
|
||||||
|
- [ ] Review changes
|
||||||
|
- capa https://github.com/fireeye/capa/compare/\<last-release\>...master
|
||||||
|
- capa-rules https://github.com/fireeye/capa-rules/compare/\<last-release>\...master
|
||||||
|
- [ ] Update [CHANGELOG.md](https://github.com/fireeye/capa/blob/master/CHANGELOG.md)
|
||||||
|
- Do not forget to add a nice introduction thanking contributors
|
||||||
|
- Remember that we need a major release if we introduce breaking changes
|
||||||
|
- Sections
|
||||||
|
- New Features
|
||||||
|
- New Rules
|
||||||
|
- Bug Fixes
|
||||||
|
- Changes
|
||||||
|
- Development
|
||||||
|
- Raw diffs
|
||||||
|
- Update `Raw diffs` links
|
||||||
|
- Create placeholder for `master (unreleased)` section
|
||||||
|
```
|
||||||
|
## master (unreleased)
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
### New Rules
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
### Raw diffs
|
||||||
|
- [capa <release>...master](https://github.com/fireeye/capa/compare/<release>...master)
|
||||||
|
- [capa-rules <release>...master](https://github.com/fireeye/capa-rules/compare/<release>...master)
|
||||||
|
```
|
||||||
|
- [ ] Update [capa/version.py](https://github.com/fireeye/capa/blob/master/capa/version.py)
|
||||||
|
- [ ] Create a PR with the updated [CHANGELOG.md](https://github.com/fireeye/capa/blob/master/CHANGELOG.md) and [capa/version.py](https://github.com/fireeye/capa/blob/master/capa/version.py). Copy this checklist in the PR description.
|
||||||
|
- [ ] After PR review, merge the PR and [create the release in GH](https://github.com/fireeye/capa/releases/new) using text from the [CHANGELOG.md](https://github.com/fireeye/capa/blob/master/CHANGELOG.md).
|
||||||
|
- [ ] Verify GH actions [upload artifacts](https://github.com/fireeye/capa/releases), [publish to PyPI](https://pypi.org/project/flare-capa) and [create a tag in capa rules](https://github.com/fireeye/capa-rules/tags) upon completion.
|
||||||
|
- [ ] [Spread the word](https://twitter.com)
|
||||||
|
|
||||||
2
rules
@@ -1,220 +1,220 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
"""
|
"""
|
||||||
bulk-process
|
bulk-process
|
||||||
|
|
||||||
Invoke capa recursively against a directory of samples
|
Invoke capa recursively against a directory of samples
|
||||||
and emit a JSON document mapping the file paths to their results.
|
and emit a JSON document mapping the file paths to their results.
|
||||||
|
|
||||||
By default, this will use subprocesses for parallelism.
|
By default, this will use subprocesses for parallelism.
|
||||||
Use `-n/--parallelism` to change the subprocess count from
|
Use `-n/--parallelism` to change the subprocess count from
|
||||||
the default of current CPU count.
|
the default of current CPU count.
|
||||||
Use `--no-mp` to use threads instead of processes,
|
Use `--no-mp` to use threads instead of processes,
|
||||||
which is probably not useful unless you set `--parallelism=1`.
|
which is probably not useful unless you set `--parallelism=1`.
|
||||||
|
|
||||||
example:
|
example:
|
||||||
|
|
||||||
$ python scripts/bulk-process /tmp/suspicious
|
$ python scripts/bulk-process /tmp/suspicious
|
||||||
{
|
{
|
||||||
"/tmp/suspicious/suspicious.dll_": {
|
"/tmp/suspicious/suspicious.dll_": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"encode data using XOR": {
|
"encode data using XOR": {
|
||||||
"matches": {
|
"matches": {
|
||||||
"268440358": {
|
"268440358": {
|
||||||
[...]
|
[...]
|
||||||
"/tmp/suspicious/1.dll_": { ... }
|
"/tmp/suspicious/1.dll_": { ... }
|
||||||
"/tmp/suspicious/2.dll_": { ... }
|
"/tmp/suspicious/2.dll_": { ... }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
usage:
|
usage:
|
||||||
|
|
||||||
usage: bulk-process.py [-h] [-r RULES] [-d] [-q] [-n PARALLELISM] [--no-mp]
|
usage: bulk-process.py [-h] [-r RULES] [-d] [-q] [-n PARALLELISM] [--no-mp]
|
||||||
input
|
input
|
||||||
|
|
||||||
detect capabilities in programs.
|
detect capabilities in programs.
|
||||||
|
|
||||||
positional arguments:
|
positional arguments:
|
||||||
input Path to directory of files to recursively analyze
|
input Path to directory of files to recursively analyze
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-r RULES, --rules RULES
|
-r RULES, --rules RULES
|
||||||
Path to rule file or directory, use embedded rules by
|
Path to rule file or directory, use embedded rules by
|
||||||
default
|
default
|
||||||
-d, --debug Enable debugging output on STDERR
|
-d, --debug Enable debugging output on STDERR
|
||||||
-q, --quiet Disable all output but errors
|
-q, --quiet Disable all output but errors
|
||||||
-n PARALLELISM, --parallelism PARALLELISM
|
-n PARALLELISM, --parallelism PARALLELISM
|
||||||
parallelism factor
|
parallelism factor
|
||||||
--no-mp disable subprocesses
|
--no-mp disable subprocesses
|
||||||
|
|
||||||
Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
|
Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
You may obtain a copy of the License at: [package root]/LICENSE.txt
|
You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||||
Unless required by applicable law or agreed to in writing, software distributed under the License
|
Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and limitations under the License.
|
See the License for the specific language governing permissions and limitations under the License.
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
import argparse
|
import argparse
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import multiprocessing.pool
|
import multiprocessing.pool
|
||||||
|
|
||||||
import capa
|
import capa
|
||||||
import capa.main
|
import capa.main
|
||||||
import capa.rules
|
import capa.rules
|
||||||
import capa.render
|
import capa.render
|
||||||
|
|
||||||
logger = logging.getLogger("capa")
|
logger = logging.getLogger("capa")
|
||||||
|
|
||||||
|
|
||||||
def get_capa_results(args):
|
def get_capa_results(args):
|
||||||
"""
|
"""
|
||||||
run capa against the file at the given path, using the given rules.
|
run capa against the file at the given path, using the given rules.
|
||||||
|
|
||||||
args is a tuple, containing:
|
args is a tuple, containing:
|
||||||
rules (capa.rules.RuleSet): the rules to match
|
rules (capa.rules.RuleSet): the rules to match
|
||||||
format (str): the name of the sample file format
|
format (str): the name of the sample file format
|
||||||
path (str): the file system path to the sample to process
|
path (str): the file system path to the sample to process
|
||||||
|
|
||||||
args is a tuple because i'm not quite sure how to unpack multiple arguments using `map`.
|
args is a tuple because i'm not quite sure how to unpack multiple arguments using `map`.
|
||||||
|
|
||||||
returns an dict with two required keys:
|
returns an dict with two required keys:
|
||||||
path (str): the file system path of the sample to process
|
path (str): the file system path of the sample to process
|
||||||
status (str): either "error" or "ok"
|
status (str): either "error" or "ok"
|
||||||
|
|
||||||
when status == "error", then a human readable message is found in property "error".
|
when status == "error", then a human readable message is found in property "error".
|
||||||
when status == "ok", then the capa results are found in the property "ok".
|
when status == "ok", then the capa results are found in the property "ok".
|
||||||
|
|
||||||
the capa results are a dictionary with the following keys:
|
the capa results are a dictionary with the following keys:
|
||||||
meta (dict): the meta analysis results
|
meta (dict): the meta analysis results
|
||||||
capabilities (dict): the matched capabilities and their result objects
|
capabilities (dict): the matched capabilities and their result objects
|
||||||
"""
|
"""
|
||||||
rules, format, path = args
|
rules, format, path = args
|
||||||
logger.info("computing capa results for: %s", path)
|
logger.info("computing capa results for: %s", path)
|
||||||
try:
|
try:
|
||||||
extractor = capa.main.get_extractor(path, format, capa.main.BACKEND_VIV, disable_progress=True)
|
extractor = capa.main.get_extractor(path, format, capa.main.BACKEND_VIV, disable_progress=True)
|
||||||
except capa.main.UnsupportedFormatError:
|
except capa.main.UnsupportedFormatError:
|
||||||
# i'm 100% sure if multiprocessing will reliably raise exceptions across process boundaries.
|
# i'm 100% sure if multiprocessing will reliably raise exceptions across process boundaries.
|
||||||
# so instead, return an object with explicit success/failure status.
|
# so instead, return an object with explicit success/failure status.
|
||||||
#
|
#
|
||||||
# if success, then status=ok, and results found in property "ok"
|
# if success, then status=ok, and results found in property "ok"
|
||||||
# if error, then status=error, and human readable message in property "error"
|
# if error, then status=error, and human readable message in property "error"
|
||||||
return {
|
return {
|
||||||
"path": path,
|
"path": path,
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"error": "input file does not appear to be a PE file: %s" % path,
|
"error": "input file does not appear to be a PE file: %s" % path,
|
||||||
}
|
}
|
||||||
except capa.main.UnsupportedRuntimeError:
|
except capa.main.UnsupportedRuntimeError:
|
||||||
return {
|
return {
|
||||||
"path": path,
|
"path": path,
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"error": "unsupported runtime or Python interpreter",
|
"error": "unsupported runtime or Python interpreter",
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {
|
||||||
"path": path,
|
"path": path,
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"error": "unexpected error: %s" % (e),
|
"error": "unexpected error: %s" % (e),
|
||||||
}
|
}
|
||||||
|
|
||||||
meta = capa.main.collect_metadata("", path, "", format, extractor)
|
meta = capa.main.collect_metadata("", path, "", format, extractor)
|
||||||
capabilities, counts = capa.main.find_capabilities(rules, extractor, disable_progress=True)
|
capabilities, counts = capa.main.find_capabilities(rules, extractor, disable_progress=True)
|
||||||
meta["analysis"].update(counts)
|
meta["analysis"].update(counts)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"path": path,
|
"path": path,
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"ok": {
|
"ok": {
|
||||||
"meta": meta,
|
"meta": meta,
|
||||||
"capabilities": capabilities,
|
"capabilities": capabilities,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
def main(argv=None):
|
||||||
if argv is None:
|
if argv is None:
|
||||||
argv = sys.argv[1:]
|
argv = sys.argv[1:]
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="detect capabilities in programs.")
|
parser = argparse.ArgumentParser(description="detect capabilities in programs.")
|
||||||
capa.main.install_common_args(parser, wanted={"rules"})
|
capa.main.install_common_args(parser, wanted={"rules"})
|
||||||
parser.add_argument("input", type=str, help="Path to directory of files to recursively analyze")
|
parser.add_argument("input", type=str, help="Path to directory of files to recursively analyze")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-n", "--parallelism", type=int, default=multiprocessing.cpu_count(), help="parallelism factor"
|
"-n", "--parallelism", type=int, default=multiprocessing.cpu_count(), help="parallelism factor"
|
||||||
)
|
)
|
||||||
parser.add_argument("--no-mp", action="store_true", help="disable subprocesses")
|
parser.add_argument("--no-mp", action="store_true", help="disable subprocesses")
|
||||||
args = parser.parse_args(args=argv)
|
args = parser.parse_args(args=argv)
|
||||||
capa.main.handle_common_args(args)
|
capa.main.handle_common_args(args)
|
||||||
|
|
||||||
if args.rules == "(embedded rules)":
|
if args.rules == "(embedded rules)":
|
||||||
logger.info("using default embedded rules")
|
logger.info("using default embedded rules")
|
||||||
logger.debug("detected running from source")
|
logger.debug("detected running from source")
|
||||||
args.rules = os.path.join(os.path.dirname(__file__), "..", "rules")
|
args.rules = os.path.join(os.path.dirname(__file__), "..", "rules")
|
||||||
logger.debug("default rule path (source method): %s", args.rules)
|
logger.debug("default rule path (source method): %s", args.rules)
|
||||||
else:
|
else:
|
||||||
logger.info("using rules path: %s", args.rules)
|
logger.info("using rules path: %s", args.rules)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rules = capa.main.get_rules(args.rules)
|
rules = capa.main.get_rules(args.rules)
|
||||||
rules = capa.rules.RuleSet(rules)
|
rules = capa.rules.RuleSet(rules)
|
||||||
logger.info("successfully loaded %s rules", len(rules))
|
logger.info("successfully loaded %s rules", len(rules))
|
||||||
except (IOError, capa.rules.InvalidRule, capa.rules.InvalidRuleSet) as e:
|
except (IOError, capa.rules.InvalidRule, capa.rules.InvalidRuleSet) as e:
|
||||||
logger.error("%s", str(e))
|
logger.error("%s", str(e))
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
samples = []
|
samples = []
|
||||||
for (base, directories, files) in os.walk(args.input):
|
for (base, directories, files) in os.walk(args.input):
|
||||||
for file in files:
|
for file in files:
|
||||||
samples.append(os.path.join(base, file))
|
samples.append(os.path.join(base, file))
|
||||||
|
|
||||||
def pmap(f, args, parallelism=multiprocessing.cpu_count()):
|
def pmap(f, args, parallelism=multiprocessing.cpu_count()):
|
||||||
"""apply the given function f to the given args using subprocesses"""
|
"""apply the given function f to the given args using subprocesses"""
|
||||||
return multiprocessing.Pool(parallelism).imap(f, args)
|
return multiprocessing.Pool(parallelism).imap(f, args)
|
||||||
|
|
||||||
def tmap(f, args, parallelism=multiprocessing.cpu_count()):
|
def tmap(f, args, parallelism=multiprocessing.cpu_count()):
|
||||||
"""apply the given function f to the given args using threads"""
|
"""apply the given function f to the given args using threads"""
|
||||||
return multiprocessing.pool.ThreadPool(parallelism).imap(f, args)
|
return multiprocessing.pool.ThreadPool(parallelism).imap(f, args)
|
||||||
|
|
||||||
def map(f, args, parallelism=None):
|
def map(f, args, parallelism=None):
|
||||||
"""apply the given function f to the given args in the current thread"""
|
"""apply the given function f to the given args in the current thread"""
|
||||||
for arg in args:
|
for arg in args:
|
||||||
yield f(arg)
|
yield f(arg)
|
||||||
|
|
||||||
if args.no_mp:
|
if args.no_mp:
|
||||||
if args.parallelism == 1:
|
if args.parallelism == 1:
|
||||||
logger.debug("using current thread mapper")
|
logger.debug("using current thread mapper")
|
||||||
mapper = map
|
mapper = map
|
||||||
else:
|
else:
|
||||||
logger.debug("using threading mapper")
|
logger.debug("using threading mapper")
|
||||||
mapper = tmap
|
mapper = tmap
|
||||||
else:
|
else:
|
||||||
logger.debug("using process mapper")
|
logger.debug("using process mapper")
|
||||||
mapper = pmap
|
mapper = pmap
|
||||||
|
|
||||||
results = {}
|
results = {}
|
||||||
for result in mapper(
|
for result in mapper(
|
||||||
get_capa_results, [(rules, "pe", sample) for sample in samples], parallelism=args.parallelism
|
get_capa_results, [(rules, "pe", sample) for sample in samples], parallelism=args.parallelism
|
||||||
):
|
):
|
||||||
if result["status"] == "error":
|
if result["status"] == "error":
|
||||||
logger.warning(result["error"])
|
logger.warning(result["error"])
|
||||||
elif result["status"] == "ok":
|
elif result["status"] == "ok":
|
||||||
meta = result["ok"]["meta"]
|
meta = result["ok"]["meta"]
|
||||||
capabilities = result["ok"]["capabilities"]
|
capabilities = result["ok"]["capabilities"]
|
||||||
# our renderer expects to emit a json document for a single sample
|
# our renderer expects to emit a json document for a single sample
|
||||||
# so we deserialize the json document, store it in a larger dict, and we'll subsequently re-encode.
|
# so we deserialize the json document, store it in a larger dict, and we'll subsequently re-encode.
|
||||||
results[result["path"]] = json.loads(capa.render.render_json(meta, rules, capabilities))
|
results[result["path"]] = json.loads(capa.render.render_json(meta, rules, capabilities))
|
||||||
else:
|
else:
|
||||||
raise ValueError("unexpected status: %s" % (result["status"]))
|
raise ValueError("unexpected status: %s" % (result["status"]))
|
||||||
|
|
||||||
print(json.dumps(results))
|
print(json.dumps(results))
|
||||||
|
|
||||||
logger.info("done.")
|
logger.info("done.")
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ def main(argv=None):
|
|||||||
return 0
|
return 0
|
||||||
else:
|
else:
|
||||||
logger.info("rule requires reformatting (%s)", rule.name)
|
logger.info("rule requires reformatting (%s)", rule.name)
|
||||||
|
if "\r\n" in rule.definition:
|
||||||
|
logger.info("please make sure that the file uses LF (\\n) line endings only")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
if args.in_place:
|
if args.in_place:
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import argparse
|
|||||||
import itertools
|
import itertools
|
||||||
import posixpath
|
import posixpath
|
||||||
|
|
||||||
|
import ruamel.yaml
|
||||||
|
|
||||||
import capa.main
|
import capa.main
|
||||||
import capa.rules
|
import capa.rules
|
||||||
import capa.engine
|
import capa.engine
|
||||||
@@ -301,6 +303,16 @@ class FeatureNtdllNtoskrnlApi(Lint):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class FormatLineFeedEOL(Lint):
|
||||||
|
name = "line(s) end with CRLF (\\r\\n)"
|
||||||
|
recommendation = "convert line endings to LF (\\n) for example using dos2unix"
|
||||||
|
|
||||||
|
def check_rule(self, ctx, rule):
|
||||||
|
if len(rule.definition.split("\r\n")) > 0:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class FormatSingleEmptyLineEOF(Lint):
|
class FormatSingleEmptyLineEOF(Lint):
|
||||||
name = "EOF format"
|
name = "EOF format"
|
||||||
recommendation = "end file with a single empty line"
|
recommendation = "end file with a single empty line"
|
||||||
@@ -321,12 +333,43 @@ class FormatIncorrect(Lint):
|
|||||||
|
|
||||||
if actual != expected:
|
if actual != expected:
|
||||||
diff = difflib.ndiff(actual.splitlines(1), expected.splitlines(True))
|
diff = difflib.ndiff(actual.splitlines(1), expected.splitlines(True))
|
||||||
self.recommendation = self.recommendation_template.format("".join(diff))
|
recommendation_template = self.recommendation_template
|
||||||
|
if "\r\n" in actual:
|
||||||
|
recommendation_template = (
|
||||||
|
self.recommendation_template + "\nplease make sure that the file uses LF (\\n) line endings only"
|
||||||
|
)
|
||||||
|
self.recommendation = recommendation_template.format("".join(diff))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class FormatStringQuotesIncorrect(Lint):
|
||||||
|
name = "rule string quotes incorrect"
|
||||||
|
|
||||||
|
def check_rule(self, ctx, rule):
|
||||||
|
events = capa.rules.Rule._get_ruamel_yaml_parser().parse(rule.definition)
|
||||||
|
for key in events:
|
||||||
|
if not (isinstance(key, ruamel.yaml.ScalarEvent) and key.value == "string"):
|
||||||
|
continue
|
||||||
|
value = next(events) # assume value is next event
|
||||||
|
if not isinstance(value, ruamel.yaml.ScalarEvent):
|
||||||
|
# ignore non-scalar
|
||||||
|
continue
|
||||||
|
if value.value.startswith("/") and value.value.endswith(("/", "/i")):
|
||||||
|
# ignore regex for now
|
||||||
|
continue
|
||||||
|
if value.style is None:
|
||||||
|
# no quotes
|
||||||
|
self.recommendation = 'add double quotes to "%s"' % value.value
|
||||||
|
return True
|
||||||
|
if value.style == "'":
|
||||||
|
# single quote
|
||||||
|
self.recommendation = 'change single quotes to double quotes for "%s"' % value.value
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def run_lints(lints, ctx, rule):
|
def run_lints(lints, ctx, rule):
|
||||||
for lint in lints:
|
for lint in lints:
|
||||||
if lint.check_rule(ctx, rule):
|
if lint.check_rule(ctx, rule):
|
||||||
@@ -385,7 +428,9 @@ def lint_features(ctx, rule):
|
|||||||
|
|
||||||
|
|
||||||
FORMAT_LINTS = (
|
FORMAT_LINTS = (
|
||||||
|
FormatLineFeedEOL(),
|
||||||
FormatSingleEmptyLineEOF(),
|
FormatSingleEmptyLineEOF(),
|
||||||
|
FormatStringQuotesIncorrect(),
|
||||||
FormatIncorrect(),
|
FormatIncorrect(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -554,7 +599,7 @@ def main(argv=None):
|
|||||||
|
|
||||||
samples_path = os.path.join(os.path.dirname(__file__), "..", "tests", "data")
|
samples_path = os.path.join(os.path.dirname(__file__), "..", "tests", "data")
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="A program.")
|
parser = argparse.ArgumentParser(description="Lint capa rules.")
|
||||||
capa.main.install_common_args(parser, wanted={"tag"})
|
capa.main.install_common_args(parser, wanted={"tag"})
|
||||||
parser.add_argument("rules", type=str, help="Path to rules")
|
parser.add_argument("rules", type=str, help="Path to rules")
|
||||||
parser.add_argument("--samples", type=str, default=samples_path, help="Path to samples")
|
parser.add_argument("--samples", type=str, default=samples_path, help="Path to samples")
|
||||||
@@ -566,8 +611,12 @@ def main(argv=None):
|
|||||||
args = parser.parse_args(args=argv)
|
args = parser.parse_args(args=argv)
|
||||||
capa.main.handle_common_args(args)
|
capa.main.handle_common_args(args)
|
||||||
|
|
||||||
logging.getLogger("capa").setLevel(logging.CRITICAL)
|
if args.debug:
|
||||||
logging.getLogger("viv_utils").setLevel(logging.CRITICAL)
|
logging.getLogger("capa").setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger("viv_utils").setLevel(logging.DEBUG)
|
||||||
|
else:
|
||||||
|
logging.getLogger("capa").setLevel(logging.ERROR)
|
||||||
|
logging.getLogger("viv_utils").setLevel(logging.ERROR)
|
||||||
|
|
||||||
time0 = time.time()
|
time0 = time.time()
|
||||||
|
|
||||||
|
|||||||
42
setup.py
@@ -12,32 +12,32 @@ import sys
|
|||||||
import setuptools
|
import setuptools
|
||||||
|
|
||||||
requirements = [
|
requirements = [
|
||||||
"six",
|
"six==1.15.0",
|
||||||
"tqdm",
|
"tqdm==4.60.0",
|
||||||
"pyyaml",
|
"pyyaml==5.4.1",
|
||||||
"tabulate",
|
"tabulate==0.8.9",
|
||||||
"colorama",
|
"colorama==0.4.4",
|
||||||
"termcolor",
|
"termcolor==1.1.0",
|
||||||
"ruamel.yaml",
|
"wcwidth==0.2.5",
|
||||||
"wcwidth",
|
|
||||||
"ida-settings==2.1.0",
|
"ida-settings==2.1.0",
|
||||||
|
"viv-utils==0.6.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
if sys.version_info >= (3, 0):
|
if sys.version_info >= (3, 0):
|
||||||
# py3
|
# py3
|
||||||
requirements.append("halo")
|
requirements.append("halo==0.0.31")
|
||||||
requirements.append("networkx")
|
requirements.append("networkx==2.5.1")
|
||||||
requirements.append("vivisect==1.0.0")
|
requirements.append("ruamel.yaml==0.17.0")
|
||||||
requirements.append("viv-utils==0.3.19")
|
requirements.append("vivisect==1.0.1")
|
||||||
requirements.append("smda==1.5.13")
|
requirements.append("smda==1.5.13")
|
||||||
else:
|
else:
|
||||||
# py2
|
# py2
|
||||||
requirements.append("enum34==1.1.6") # v1.1.6 is needed by halo 0.0.30 / spinners 0.0.24
|
requirements.append("enum34==1.1.6") # v1.1.6 is needed by halo 0.0.30 / spinners 0.0.24
|
||||||
requirements.append("halo==0.0.30") # halo==0.0.30 is the last version to support py2.7
|
requirements.append("halo==0.0.30") # halo==0.0.30 is the last version to support py2.7
|
||||||
requirements.append("vivisect==0.2.1")
|
requirements.append("vivisect==0.2.1")
|
||||||
requirements.append("viv-utils==0.3.19")
|
|
||||||
requirements.append("networkx==2.2") # v2.2 is last version supported by Python 2.7
|
requirements.append("networkx==2.2") # v2.2 is last version supported by Python 2.7
|
||||||
requirements.append("backports.functools-lru-cache")
|
requirements.append("ruamel.yaml==0.16.13") # last version tested with Python 2.7
|
||||||
|
requirements.append("backports.functools-lru-cache==1.6.1")
|
||||||
|
|
||||||
# this sets __version__
|
# this sets __version__
|
||||||
# via: http://stackoverflow.com/a/7071358/87207
|
# via: http://stackoverflow.com/a/7071358/87207
|
||||||
@@ -77,13 +77,13 @@ setuptools.setup(
|
|||||||
install_requires=requirements,
|
install_requires=requirements,
|
||||||
extras_require={
|
extras_require={
|
||||||
"dev": [
|
"dev": [
|
||||||
"pytest",
|
"pytest==4.6.11", # TODO: Change to 6.2.3 when removing py2
|
||||||
"pytest-sugar",
|
"pytest-sugar==0.9.4",
|
||||||
"pytest-instafail",
|
"pytest-instafail==0.4.2",
|
||||||
"pytest-cov",
|
"pytest-cov==2.11.1",
|
||||||
"pycodestyle",
|
"pycodestyle==2.7.0",
|
||||||
"black ; python_version>'3.0'",
|
"black==20.8b1 ; python_version>'3.0'",
|
||||||
"isort",
|
"isort==4.3.21", # TODO: Change to 5.8.0 when removing py2
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
|
|||||||
@@ -1,104 +1,104 @@
|
|||||||
# run this script from within IDA with ./tests/data/mimikatz.exe open
|
# run this script from within IDA with ./tests/data/mimikatz.exe open
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
import binascii
|
import binascii
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sys.path.append(os.path.dirname(__file__))
|
sys.path.append(os.path.dirname(__file__))
|
||||||
from fixtures import *
|
from fixtures import *
|
||||||
finally:
|
finally:
|
||||||
sys.path.pop()
|
sys.path.pop()
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("test_ida_features")
|
logger = logging.getLogger("test_ida_features")
|
||||||
|
|
||||||
|
|
||||||
def check_input_file(wanted):
|
def check_input_file(wanted):
|
||||||
import idautils
|
import idautils
|
||||||
|
|
||||||
# some versions (7.4) of IDA return a truncated version of the MD5.
|
# some versions (7.4) of IDA return a truncated version of the MD5.
|
||||||
# https://github.com/idapython/bin/issues/11
|
# https://github.com/idapython/bin/issues/11
|
||||||
try:
|
try:
|
||||||
found = idautils.GetInputFileMD5()[:31].decode("ascii").lower()
|
found = idautils.GetInputFileMD5()[:31].decode("ascii").lower()
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
# in IDA 7.5 or so, GetInputFileMD5 started returning raw binary
|
# in IDA 7.5 or so, GetInputFileMD5 started returning raw binary
|
||||||
# rather than the hex digest
|
# rather than the hex digest
|
||||||
found = binascii.hexlify(idautils.GetInputFileMD5()[:15]).decode("ascii").lower()
|
found = binascii.hexlify(idautils.GetInputFileMD5()[:15]).decode("ascii").lower()
|
||||||
|
|
||||||
if not wanted.startswith(found):
|
if not wanted.startswith(found):
|
||||||
raise RuntimeError("please run the tests against sample with MD5: `%s`" % (wanted))
|
raise RuntimeError("please run the tests against sample with MD5: `%s`" % (wanted))
|
||||||
|
|
||||||
|
|
||||||
def get_ida_extractor(_path):
|
def get_ida_extractor(_path):
|
||||||
check_input_file("5f66b82558ca92e54e77f216ef4c066c")
|
check_input_file("5f66b82558ca92e54e77f216ef4c066c")
|
||||||
|
|
||||||
# have to import import this inline so pytest doesn't bail outside of IDA
|
# have to import import this inline so pytest doesn't bail outside of IDA
|
||||||
import capa.features.extractors.ida
|
import capa.features.extractors.ida
|
||||||
|
|
||||||
return capa.features.extractors.ida.IdaFeatureExtractor()
|
return capa.features.extractors.ida.IdaFeatureExtractor()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="IDA Pro tests must be run within IDA")
|
@pytest.mark.skip(reason="IDA Pro tests must be run within IDA")
|
||||||
def test_ida_features():
|
def test_ida_features():
|
||||||
for (sample, scope, feature, expected) in FEATURE_PRESENCE_TESTS + FEATURE_PRESENCE_TESTS_IDA:
|
for (sample, scope, feature, expected) in FEATURE_PRESENCE_TESTS + FEATURE_PRESENCE_TESTS_IDA:
|
||||||
id = make_test_id((sample, scope, feature, expected))
|
id = make_test_id((sample, scope, feature, expected))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
check_input_file(get_sample_md5_by_name(sample))
|
check_input_file(get_sample_md5_by_name(sample))
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
print("SKIP %s" % (id))
|
print("SKIP %s" % (id))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
scope = resolve_scope(scope)
|
scope = resolve_scope(scope)
|
||||||
sample = resolve_sample(sample)
|
sample = resolve_sample(sample)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
do_test_feature_presence(get_ida_extractor, sample, scope, feature, expected)
|
do_test_feature_presence(get_ida_extractor, sample, scope, feature, expected)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("FAIL %s" % (id))
|
print("FAIL %s" % (id))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
else:
|
else:
|
||||||
print("OK %s" % (id))
|
print("OK %s" % (id))
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="IDA Pro tests must be run within IDA")
|
@pytest.mark.skip(reason="IDA Pro tests must be run within IDA")
|
||||||
def test_ida_feature_counts():
|
def test_ida_feature_counts():
|
||||||
for (sample, scope, feature, expected) in FEATURE_COUNT_TESTS:
|
for (sample, scope, feature, expected) in FEATURE_COUNT_TESTS:
|
||||||
id = make_test_id((sample, scope, feature, expected))
|
id = make_test_id((sample, scope, feature, expected))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
check_input_file(get_sample_md5_by_name(sample))
|
check_input_file(get_sample_md5_by_name(sample))
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
print("SKIP %s" % (id))
|
print("SKIP %s" % (id))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
scope = resolve_scope(scope)
|
scope = resolve_scope(scope)
|
||||||
sample = resolve_sample(sample)
|
sample = resolve_sample(sample)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
do_test_feature_count(get_ida_extractor, sample, scope, feature, expected)
|
do_test_feature_count(get_ida_extractor, sample, scope, feature, expected)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("FAIL %s" % (id))
|
print("FAIL %s" % (id))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
else:
|
else:
|
||||||
print("OK %s" % (id))
|
print("OK %s" % (id))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print("-" * 80)
|
print("-" * 80)
|
||||||
|
|
||||||
# invoke all functions in this module that start with `test_`
|
# invoke all functions in this module that start with `test_`
|
||||||
for name in dir(sys.modules[__name__]):
|
for name in dir(sys.modules[__name__]):
|
||||||
if not name.startswith("test_"):
|
if not name.startswith("test_"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
test = getattr(sys.modules[__name__], name)
|
test = getattr(sys.modules[__name__], name)
|
||||||
logger.debug("invoking test: %s", name)
|
logger.debug("invoking test: %s", name)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
test()
|
test()
|
||||||
|
|
||||||
print("DONE")
|
print("DONE")
|
||||||
|
|||||||
@@ -681,6 +681,25 @@ def test_explicit_string_values_int():
|
|||||||
assert (String("0x123") in children) == True
|
assert (String("0x123") in children) == True
|
||||||
|
|
||||||
|
|
||||||
|
def test_string_values_special_characters():
|
||||||
|
rule = textwrap.dedent(
|
||||||
|
"""
|
||||||
|
rule:
|
||||||
|
meta:
|
||||||
|
name: test rule
|
||||||
|
features:
|
||||||
|
- or:
|
||||||
|
- string: "hello\\r\\nworld"
|
||||||
|
- string: "bye\\nbye"
|
||||||
|
description: "test description"
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
r = capa.rules.Rule.from_yaml(rule)
|
||||||
|
children = list(r.statement.get_children())
|
||||||
|
assert (String("hello\r\nworld") in children) == True
|
||||||
|
assert (String("bye\nbye") in children) == True
|
||||||
|
|
||||||
|
|
||||||
def test_regex_values_always_string():
|
def test_regex_values_always_string():
|
||||||
rules = [
|
rules = [
|
||||||
capa.rules.Rule.from_yaml(
|
capa.rules.Rule.from_yaml(
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ from fixtures import *
|
|||||||
FEATURE_PRESENCE_TESTS,
|
FEATURE_PRESENCE_TESTS,
|
||||||
indirect=["sample", "scope"],
|
indirect=["sample", "scope"],
|
||||||
)
|
)
|
||||||
|
@pytest.mark.xfail(sys.version_info < (3, 0), reason="SMDA only works on py3")
|
||||||
|
@pytest.mark.xfail(sys.platform == "win32", reason="SMDA bug: https://github.com/danielplohmann/smda/issues/20")
|
||||||
def test_smda_features(sample, scope, feature, expected):
|
def test_smda_features(sample, scope, feature, expected):
|
||||||
with xfail(sys.version_info < (3, 0), reason="SMDA only works on py3"):
|
do_test_feature_presence(get_smda_extractor, sample, scope, feature, expected)
|
||||||
do_test_feature_presence(get_smda_extractor, sample, scope, feature, expected)
|
|
||||||
|
|
||||||
|
|
||||||
@parametrize(
|
@parametrize(
|
||||||
|
|||||||