Skip to content

Commit 6f03a14

Browse files
authored
Merge pull request #25 from TheHive-Project/24-extend-the-project-with-github-actions-to-auto-release-to-pypi-on-version-tagging
#24 - Add initial cicd actions and comply with them
2 parents 561f7ff + 1c8c6e3 commit 6f03a14

File tree

14 files changed

+505
-352
lines changed

14 files changed

+505
-352
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: build-package
2+
on:
3+
workflow_call:
4+
jobs:
5+
build:
6+
name: Build wheel and sdist
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v4
10+
- name: Set up Python
11+
uses: actions/setup-python@v4
12+
with:
13+
python-version: 3.13
14+
- name: Install build dependencies
15+
run: pip install --no-cache-dir -U pip . build twine
16+
- name: Build package
17+
run: python -m build --sdist --wheel
18+
- name: Upload built distributions
19+
uses: actions/upload-artifact@v4
20+
with:
21+
name: dist
22+
path: dist
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: static-checks
2+
on:
3+
workflow_call:
4+
jobs:
5+
static-checks:
6+
name: Run static checks
7+
runs-on: ubuntu-latest
8+
strategy:
9+
matrix:
10+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
11+
steps:
12+
- uses: actions/checkout@v4
13+
- name: Set up Python ${{ matrix.python-version }}
14+
uses: actions/setup-python@v4
15+
with:
16+
python-version: ${{ matrix.python-version }}
17+
- name: Install dependencies
18+
run: pip install --no-cache-dir -U pip . black flake8 bandit
19+
- name: Lint check with flake8
20+
run: flake8 cortexutils/ tests/ setup.py
21+
- name: Format check with black
22+
run: black --check cortexutils/ tests/ setup.py
23+
- name: Security check with bandit
24+
run: bandit -r cortexutils/

.github/workflows/_unit-tests.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: unit-tests
2+
on:
3+
workflow_call:
4+
jobs:
5+
unit-tests:
6+
name: Run unit tests
7+
runs-on: ubuntu-latest
8+
strategy:
9+
matrix:
10+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
11+
steps:
12+
- uses: actions/checkout@v4
13+
- name: Set up Python
14+
uses: actions/setup-python@v4
15+
with:
16+
python-version: ${{ matrix.python-version }}
17+
- name: Install dependencies
18+
run: pip install --no-cache-dir -U pip .
19+
- name: Run unit tests
20+
run: python -m unittest --verbose
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: upload-package
2+
on:
3+
workflow_call:
4+
secrets:
5+
PYPI_TOKEN:
6+
required: true
7+
jobs:
8+
upload:
9+
name: Upload wheel and sdist
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- name: Compare tag and package version
14+
run: |
15+
TAG=${GITHUB_REF#refs/*/}
16+
VERSION=$(grep -Po '(?<=version=")[^"]*' setup.py)
17+
if [ "$TAG" != "$VERSION" ]; then
18+
echo "Tag value and package version are different: ${TAG} != ${VERSION}"
19+
exit 1
20+
fi
21+
- name: Download built distributions
22+
uses: actions/download-artifact@v4
23+
with:
24+
name: dist
25+
path: dist
26+
- name: Set up Python
27+
uses: actions/setup-python@v4
28+
with:
29+
python-version: 3.13
30+
- name: Install build dependencies
31+
run: pip install --no-cache-dir -U pip . twine
32+
- name: Upload to PyPI
33+
run: twine upload dist/*
34+
env:
35+
TWINE_REPOSITORY_URL: https://upload.pypi.org/legacy/
36+
TWINE_USERNAME: __token__
37+
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}

.github/workflows/main-cicd.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: cicd
2+
on:
3+
push:
4+
branches:
5+
- main
6+
tags:
7+
- "*"
8+
pull_request:
9+
jobs:
10+
static-checks:
11+
uses: ./.github/workflows/_static-checks.yml
12+
unit-tests:
13+
uses: ./.github/workflows/_unit-tests.yml
14+
build-package:
15+
uses: ./.github/workflows/_build-package.yml
16+
upload-package:
17+
if: startsWith(github.ref, 'refs/tags/')
18+
uses: ./.github/workflows/_upload-package.yml
19+
needs: [build-package, unit-tests, static-checks]
20+
secrets:
21+
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}

cortexutils/analyzer.py

Lines changed: 57 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
#!/usr/bin/env python
22
# encoding: utf-8
33

4-
import json
54
import os
6-
import stat
5+
import tempfile
6+
from shutil import copyfileobj
77

88
from cortexutils.extractor import Extractor
99
from cortexutils.worker import Worker
10-
from shutil import copyfileobj
11-
import tempfile
12-
import ntpath
1310

1411

1512
class Analyzer(Worker):
@@ -21,21 +18,27 @@ def __init__(self, job_directory=None, secret_phrases=None):
2118
self.artifact = self._input
2219

2320
# Check for auto extraction config
24-
self.auto_extract = self.get_param('config.auto_extract', self.get_param('config.auto_extract_artifacts', True))
21+
self.auto_extract = self.get_param(
22+
"config.auto_extract", self.get_param("config.auto_extract_artifacts", True)
23+
)
2524

2625
def get_data(self):
2726
"""Wrapper for getting data from input dict.
2827
2928
:return: Data (observable value) given through Cortex"""
30-
if self.data_type == 'file':
31-
return self.get_param('filename', None, 'Missing filename.')
29+
if self.data_type == "file":
30+
return self.get_param("filename", None, "Missing filename.")
3231
else:
33-
return self.get_param('data', None, 'Missing data field')
32+
return self.get_param("data", None, "Missing data field")
3433

3534
def get_param(self, name, default=None, message=None):
3635
data = super(Analyzer, self).get_param(name, default, message)
37-
if name == 'file' and self.data_type == 'file' and self.job_directory is not None:
38-
path = '%s/input/%s' % (self.job_directory, data)
36+
if (
37+
name == "file"
38+
and self.data_type == "file"
39+
and self.job_directory is not None
40+
):
41+
path = "%s/input/%s" % (self.job_directory, data)
3942
if os.path.isfile(path):
4043
return path
4144
else:
@@ -50,17 +53,19 @@ def build_taxonomy(self, level, namespace, predicate, value):
5053
:return: dict
5154
"""
5255
# Set info level if something not expected is set
53-
if level not in ['info', 'safe', 'suspicious', 'malicious']:
54-
level = 'info'
56+
if level not in ["info", "safe", "suspicious", "malicious"]:
57+
level = "info"
5558
return {
56-
'level': level,
57-
'namespace': namespace,
58-
'predicate': predicate,
59-
'value': value
59+
"level": level,
60+
"namespace": namespace,
61+
"predicate": predicate,
62+
"value": value,
6063
}
6164

6265
def summary(self, raw):
63-
"""Returns a summary, needed for 'short.html' template. Overwrite it for your needs!
66+
"""Returns a summary, needed for 'short.html' template.
67+
68+
Overwrite it for your needs!
6469
6570
:returns: by default return an empty dict"""
6671
return {}
@@ -75,20 +80,26 @@ def artifacts(self, raw):
7580
return []
7681

7782
def build_artifact(self, data_type, data, **kwargs):
78-
if data_type == 'file':
83+
if data_type == "file":
7984
if os.path.isfile(data):
8085
dst = tempfile.NamedTemporaryFile(
81-
dir=os.path.join(self.job_directory, "output"), delete=False)
82-
with open(data, 'rb') as src:
86+
dir=os.path.join(self.job_directory, "output"), delete=False
87+
)
88+
with open(data, "rb") as src:
8389
copyfileobj(src, dst)
8490
dstfname = dst.name
8591
dst.close()
8692
os.chmod(dstfname, 0o444)
87-
kwargs.update({'dataType': data_type, 'file': os.path.basename(dst.name),
88-
'filename': os.path.basename(data)})
93+
kwargs.update(
94+
{
95+
"dataType": data_type,
96+
"file": os.path.basename(dst.name),
97+
"filename": os.path.basename(data),
98+
}
99+
)
89100
return kwargs
90101
else:
91-
kwargs.update({'dataType': data_type, 'data': data})
102+
kwargs.update({"dataType": data_type, "data": data})
92103
return kwargs
93104

94105
def report(self, full_report, ensure_ascii=False):
@@ -101,40 +112,49 @@ def report(self, full_report, ensure_ascii=False):
101112
try:
102113
summary = self.summary(full_report)
103114
except Exception:
104-
pass
115+
pass # nosec B110
105116
operation_list = []
106117
try:
107118
operation_list = self.operations(full_report)
108119
except Exception:
109-
pass
110-
super(Analyzer, self).report({
111-
'success': True,
112-
'summary': summary,
113-
'artifacts': self.artifacts(full_report),
114-
'operations': operation_list,
115-
'full': full_report
116-
}, ensure_ascii)
120+
pass # nosec B110
121+
super(Analyzer, self).report(
122+
{
123+
"success": True,
124+
"summary": summary,
125+
"artifacts": self.artifacts(full_report),
126+
"operations": operation_list,
127+
"full": full_report,
128+
},
129+
ensure_ascii,
130+
)
117131

118132
def run(self):
119133
"""Overwritten by analyzers"""
120134
pass
121135

122136
# Not breaking compatibility
123137
def notSupported(self):
124-
self.error('This datatype is not supported by this analyzer.')
138+
self.error("This datatype is not supported by this analyzer.")
125139

126140
# Not breaking compatibility
127141
def unexpectedError(self, e):
128-
self.error('Unexpected Error: ' + str(e))
142+
self.error("Unexpected Error: " + str(e))
129143

130144
# Not breaking compatibility
131145
def getData(self):
132-
"""For not breaking compatibility to cortexutils.analyzer, this wraps get_data()"""
146+
"""Wrapper of get_data.
147+
148+
For not breaking compatibility to cortexutils.analyzer.
149+
"""
133150
return self.get_data()
134151

135152
# Not breaking compatibility
136153
def getParam(self, name, default=None, message=None):
137-
"""For not breaking compatibility to cortexutils.analyzer, this wraps get_param()"""
154+
"""Wrapper for get_param.
155+
156+
For not breaking compatibility to cortexutils.analyzer.
157+
"""
138158
return self.get_param(name=name, default=default, message=message)
139159

140160
# Not breaking compatibility

0 commit comments

Comments
 (0)