diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5469889a..35f47c51 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,10 +15,10 @@ jobs: # use old linux so that the shared library versioning is more portable artifact_name: capa asset_name: linux - - os: windows-latest + - os: windows-2019 artifact_name: capa.exe asset_name: windows - - os: macos-latest + - os: macos-10.15 artifact_name: capa asset_name: macos steps: @@ -30,7 +30,7 @@ jobs: uses: actions/setup-python@v2 with: python-version: 3.9 - - if: matrix.os == 'ubuntu-latest' + - if: matrix.os == 'ubuntu-16.04' run: sudo apt-get install -y libyaml-dev - name: Install PyInstaller run: pip install 'pyinstaller==4.2' @@ -47,7 +47,7 @@ jobs: zip: name: zip ${{ matrix.asset_name }} - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: build strategy: matrix: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 53768b91..a0ecb770 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,7 +9,7 @@ on: jobs: deploy: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python @@ -26,4 +26,4 @@ jobs: TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | python setup.py sdist bdist_wheel - twine upload --skip-existing dist/* \ No newline at end of file + twine upload --skip-existing dist/* diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml new file mode 100644 index 00000000..77c66d0e --- /dev/null +++ b/.github/workflows/tag.yml @@ -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 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c3faac83..14b64894 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,7 @@ on: jobs: code_style: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Checkout capa uses: actions/checkout@v2 @@ -24,7 +24,7 @@ jobs: run: black -l 120 --check . rule_linter: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Checkout capa with rules submodule uses: actions/checkout@v2 @@ -42,7 +42,7 @@ jobs: tests: name: Tests in ${{ matrix.python }} - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: [code_style, rule_linter] strategy: fail-fast: false @@ -67,4 +67,3 @@ jobs: run: pip install -e .[dev] - name: Run tests run: pytest tests/ - diff --git a/README.md b/README.md index 0b1b26c7..498a111d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flare-capa)](https://pypi.org/project/flare-capa) [![Last release](https://img.shields.io/github/v/release/fireeye/capa)](https://github.com/fireeye/capa/releases) -[![Number of rules](https://img.shields.io/badge/rules-471-blue.svg)](https://github.com/fireeye/capa-rules) +[![Number of rules](https://img.shields.io/badge/rules-470-blue.svg)](https://github.com/fireeye/capa-rules) [![CI status](https://github.com/fireeye/capa/workflows/CI/badge.svg)](https://github.com/fireeye/capa/actions?query=workflow%3ACI+event%3Apush+branch%3Amaster) [![Downloads](https://img.shields.io/github/downloads/fireeye/capa/total)](https://github.com/fireeye/capa/releases) [![License](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE.txt) diff --git a/capa/ida/plugin/form.py b/capa/ida/plugin/form.py index 212d8b90..f16f5abe 100644 --- a/capa/ida/plugin/form.py +++ b/capa/ida/plugin/form.py @@ -617,6 +617,7 @@ class CapaExplorerForm(idaapi.PluginForm): rule_path = settings.user[CAPA_SETTINGS_RULE_PATH] try: + # TODO refactor: this first part is identical to capa.main.get_rules if not os.path.exists(rule_path): raise IOError("rule path %s does not exist or cannot be accessed" % rule_path) @@ -632,8 +633,8 @@ class CapaExplorerForm(idaapi.PluginForm): continue for file in files: if not file.endswith(".yml"): - if not (file.endswith(".md") or file.endswith(".git") or file.endswith(".txt")): - # expect to see readme.md, format.md, and maybe a .git directory + if not (file.startswith(".git") or file.endswith((".git", ".md", ".txt"))): + # expect to see .git* files, readme.md, format.md, and maybe a .git directory # other things maybe are rules, but are mis-named. logger.warning("skipping non-.yml file: %s", file) continue @@ -1019,6 +1020,12 @@ class CapaExplorerForm(idaapi.PluginForm): # create deep copy of current rules, add our new rule 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) try: diff --git a/capa/ida/plugin/view.py b/capa/ida/plugin/view.py index d0fb0474..52a29916 100644 --- a/capa/ida/plugin/view.py +++ b/capa/ida/plugin/view.py @@ -415,6 +415,11 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget): # 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], "")) + 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): # take child from its parent by index, add to new parent new_parent.addChild(o.parent().takeChild(o.parent().indexOfChild(o))) @@ -425,6 +430,15 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget): def slot_edit_expression(self, action): """ """ 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) def slot_clear_all(self, action): @@ -801,9 +815,11 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget): if text: for o in iterate_tree(self): data = o.data(0, 0x100) - if data and text.lower() not in data.get_value_str().lower(): - o.setHidden(True) - continue + if data: + to_match = data.get_value_str() + if not to_match or text.lower() not in to_match.lower(): + o.setHidden(True) + continue o.setHidden(False) o.setExpanded(True) else: diff --git a/capa/main.py b/capa/main.py index 19fda0d4..41910ece 100644 --- a/capa/main.py +++ b/capa/main.py @@ -379,8 +379,8 @@ def get_rules(rule_path, disable_progress=False): for file in files: if not file.endswith(".yml"): - if not (file.endswith(".md") or file.endswith(".git") or file.endswith(".txt")): - # expect to see readme.md, format.md, and maybe a .git directory + if not (file.startswith(".git") or file.endswith((".git", ".md", ".txt"))): + # expect to see .git* files, readme.md, format.md, and maybe a .git directory # other things maybe are rules, but are mis-named. logger.warning("skipping non-.yml file: %s", file) continue diff --git a/doc/release.md b/doc/release.md new file mode 100644 index 00000000..729b648b --- /dev/null +++ b/doc/release.md @@ -0,0 +1,25 @@ +# Release checklist + +- [ ] Ensure all milestoned issues/PRs 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/\...master + - capa-rules https://github.com/fireeye/capa-rules/compare/\\...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 [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) + diff --git a/rules b/rules index a6ec6686..eb8221d9 160000 --- a/rules +++ b/rules @@ -1 +1 @@ -Subproject commit a6ec6686905be33a665099bb7046c6f5a4c4e1d1 +Subproject commit eb8221d9ad44a3d10851a281614ed8dcefb8495c diff --git a/tests/data b/tests/data index cd6defdb..e63a71b3 160000 --- a/tests/data +++ b/tests/data @@ -1 +1 @@ -Subproject commit cd6defdb2c46b309142b0867f4f97af6c48a311a +Subproject commit e63a71b394fca79f209ab718796316cce2b1a82c