Skip to content

Commit c57d234

Browse files
Akshit MaheshwaryAkshit Maheshwary
authored andcommitted
tracking rules_version and minor changes
1 parent f06c049 commit c57d234

File tree

5 files changed

+176
-27
lines changed

5 files changed

+176
-27
lines changed

api_app/analyzers_manager/file_analyzers/capa_info.py

Lines changed: 95 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@
44
import json
55
import logging
66
import os
7+
import shutil
78
import subprocess
89
from shlex import quote
910
from zipfile import ZipFile
1011

1112
import requests
1213
from django.conf import settings
14+
from django.utils import timezone
1315

1416
from api_app.analyzers_manager.classes import FileAnalyzer
1517
from api_app.analyzers_manager.exceptions import AnalyzerRunException
18+
from api_app.analyzers_manager.models import AnalyzerRulesFileVersion, PythonModule
1619
from tests.mock_utils import if_mock_connections, patch
1720

1821
logger = logging.getLogger(__name__)
@@ -28,9 +31,44 @@ class CapaInfo(FileAnalyzer):
2831
shellcode: bool
2932
arch: str
3033
timeout: float = 15
34+
force_pull_signatures: bool = False
35+
36+
def _check_if_latest_version(self, latest_version: str) -> bool:
37+
38+
analyzer_rules_file_version = AnalyzerRulesFileVersion.objects.filter(
39+
python_module=self.python_module
40+
).first()
41+
42+
if analyzer_rules_file_version is None:
43+
return False
44+
45+
return latest_version == analyzer_rules_file_version.last_downloaded_version
3146

3247
@classmethod
33-
def _unzip(cls):
48+
def _update_rules_file_version(cls, latest_version: str, file_url: str):
49+
capa_module = PythonModule.objects.get(
50+
module="capa_info.CapaInfo",
51+
base_path="api_app.analyzers_manager.file_analyzers",
52+
)
53+
54+
analyzer_rules_file_version, created = (
55+
AnalyzerRulesFileVersion.objects.update_or_create(
56+
python_module=capa_module,
57+
defaults={
58+
"last_downloaded_version": latest_version,
59+
"download_url": file_url,
60+
"downloaded_at": timezone.now(),
61+
},
62+
)
63+
)
64+
65+
if created:
66+
logger.info(f"Created new entry for {capa_module} rules file version")
67+
else:
68+
logger.info(f"Updated existing entry for {capa_module} rules file version")
69+
70+
@classmethod
71+
def _unzip_rules(cls):
3472
logger.info(f"Extracting rules at {RULES_LOCATION}")
3573
with ZipFile(RULES_FILE, mode="r") as archive:
3674
archive.extractall(
@@ -41,31 +79,46 @@ def _unzip(cls):
4179
@classmethod
4280
def _download_rules(cls, latest_version: str):
4381

44-
if not os.path.exists(RULES_LOCATION):
45-
os.makedirs(RULES_LOCATION)
82+
if os.path.exists(RULES_LOCATION):
83+
logger.info(f"Removing existing rules at {RULES_LOCATION}")
84+
shutil.rmtree(RULES_LOCATION)
85+
86+
os.makedirs(RULES_LOCATION)
87+
logger.info(f"Created fresh rules directory at {RULES_LOCATION}")
4688

4789
file_to_download = latest_version + ".zip"
4890
file_url = RULES_URL + file_to_download
4991
try:
5092

5193
response = requests.get(file_url, stream=True)
52-
logger.info(f"Started downloading rules from {file_url}")
94+
logger.info(
95+
f"Started downloading rules with version: {latest_version} from {file_url}"
96+
)
5397
with open(RULES_FILE, mode="wb+") as file:
5498
for chunk in response.iter_content(chunk_size=10 * 1024):
5599
file.write(chunk)
56100

101+
cls._update_rules_file_version(latest_version, file_url)
102+
logger.info(f"Bumped up version number in db to {latest_version}")
103+
57104
except Exception as e:
58105
logger.error(f"Failed to download rules with error: {e}")
59106
raise AnalyzerRunException("Failed to download rules")
60107

61-
logger.info(f"Rules have been successfully downloaded at {RULES_LOCATION}")
108+
logger.info(
109+
f"Rules with version: {latest_version} have been successfully downloaded at {RULES_LOCATION}"
110+
)
62111

63112
@classmethod
64113
def _download_signatures(cls) -> None:
65114
logger.info(f"Downloading signatures at {SIGNATURE_LOCATION} now")
66115

67-
if not os.path.exists(SIGNATURE_LOCATION):
68-
os.makedirs(SIGNATURE_LOCATION)
116+
if os.path.exists(SIGNATURE_LOCATION):
117+
logger.info(f"Removing existing signatures at {SIGNATURE_LOCATION}")
118+
shutil.rmtree(SIGNATURE_LOCATION)
119+
120+
os.makedirs(SIGNATURE_LOCATION)
121+
logger.info(f"Created fresh signatures directory at {SIGNATURE_LOCATION}")
69122

70123
signatures_url = "https://api.github.com/repos/mandiant/capa/contents/sigs"
71124
try:
@@ -77,28 +130,29 @@ def _download_signatures(cls) -> None:
77130
filename = signature["name"]
78131
download_url = signature["download_url"]
79132

133+
signature_file_path = os.path.join(SIGNATURE_LOCATION, filename)
134+
80135
sig_content = requests.get(download_url, stream=True)
81-
with open(filename, mode="wb") as file:
136+
with open(signature_file_path, mode="wb") as file:
82137
for chunk in sig_content.iter_content(chunk_size=10 * 1024):
83138
file.write(chunk)
84139

85140
except Exception as e:
86141
logger.error(f"Failed to download signature: {e}")
87142
raise AnalyzerRunException("Failed to update signatures")
88-
logger.info("Successfully updated singatures")
143+
logger.info("Successfully updated signatures")
89144

90145
@classmethod
91146
def update(cls) -> bool:
92147
try:
93-
logger.info("Updating capa rules and signatures")
148+
logger.info("Updating capa rules")
94149
response = requests.get(
95150
"https://api.github.com/repos/mandiant/capa-rules/releases/latest"
96151
)
97152
latest_version = response.json()["tag_name"]
98153
cls._download_rules(latest_version)
99-
cls._unzip()
100-
cls._download_signatures()
101-
logger.info("Successfully updated capa rules and signatures")
154+
cls._unzip_rules()
155+
logger.info("Successfully updated capa rules")
102156

103157
return True
104158

@@ -109,16 +163,22 @@ def update(cls) -> bool:
109163

110164
def run(self):
111165
try:
112-
if (
113-
not (
114-
os.path.isdir(RULES_LOCATION) and os.path.isdir(SIGNATURE_LOCATION)
115-
)
116-
and not self.update()
117-
):
118166

119-
raise AnalyzerRunException(
120-
"Couldn't update capa rules or signatures successfully"
121-
)
167+
response = requests.get(
168+
"https://api.github.com/repos/mandiant/capa-rules/releases/latest"
169+
)
170+
latest_version = response.json()["tag_name"]
171+
172+
update_status = (
173+
True if self._check_if_latest_version(latest_version) else self.update()
174+
)
175+
176+
if self.force_pull_signatures or not os.path.isdir(SIGNATURE_LOCATION):
177+
self._download_signatures()
178+
179+
if not (os.path.isdir(RULES_LOCATION)) and not update_status:
180+
181+
raise AnalyzerRunException("Couldn't update capa rules")
122182

123183
command: list[str] = ["/usr/local/bin/capa", "--quiet", "--json"]
124184
shell_code_arch = "sc64" if self.arch == "64" else "sc32"
@@ -136,7 +196,9 @@ def run(self):
136196

137197
command.append(quote(self.filepath))
138198

139-
logger.info(f"Starting CAPA analysis for {self.filename}")
199+
logger.info(
200+
f"Starting CAPA analysis for {self.filename} with hash: {self.md5} and command: {command}"
201+
)
140202

141203
process: subprocess.CompletedProcess = subprocess.run(
142204
command,
@@ -147,13 +209,20 @@ def run(self):
147209
)
148210

149211
result = json.loads(process.stdout)
150-
logger.info("CAPA analysis successfully completed")
212+
result["command_executed"] = command
213+
result["rules_version"] = latest_version
214+
215+
logger.info(
216+
f"CAPA analysis successfully completed for file: {self.filename} with hash {self.md5}"
217+
)
151218

152219
except subprocess.CalledProcessError as e:
153220
stderr = e.stderr
154-
logger.info(f"Capa Info failed to run for {self.filename} with command {e}")
221+
logger.info(
222+
f"Capa Info failed to run for {self.filename} with hash: {self.md5} with command {e}"
223+
)
155224
raise AnalyzerRunException(
156-
f" Analyzer for {self.filename} failed with error: {stderr}"
225+
f" Analyzer for {self.filename} with hash: {self.md5} failed with error: {stderr}"
157226
)
158227

159228
return result

api_app/analyzers_manager/migrations/0164_update_capa.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,29 @@
66
def migrate(apps, schema_editor):
77
PythonModule = apps.get_model("api_app", "PythonModule")
88
Parameter = apps.get_model("api_app", "Parameter")
9+
CrontabSchedule = apps.get_model("django_celery_beat", "CrontabSchedule")
910
AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig")
1011

1112
pm = PythonModule.objects.get(
1213
module="capa_info.CapaInfo",
1314
base_path="api_app.analyzers_manager.file_analyzers",
1415
)
1516

17+
new_crontab, created = CrontabSchedule.objects.get_or_create(
18+
minute="0",
19+
hour="0",
20+
day_of_week="*",
21+
day_of_month="*",
22+
month_of_year="*",
23+
timezone="UTC",
24+
)
25+
if created:
26+
pm.update_schedule = new_crontab
27+
pm.full_clean()
28+
pm.save()
29+
1630
AnalyzerConfig.objects.filter(python_module=pm).update(soft_time_limit=1800)
31+
AnalyzerConfig.objects.filter(python_module=pm).update(docker_based=False)
1732

1833
p1 = Parameter(
1934
name="timeout",
@@ -24,9 +39,21 @@ def migrate(apps, schema_editor):
2439
python_module=pm,
2540
)
2641

42+
p2 = Parameter(
43+
name="force_pull_signatures",
44+
type="bool",
45+
description="Force download signatures from flare-capa github repository",
46+
is_secret=False,
47+
required=False,
48+
python_module=pm,
49+
)
50+
2751
p1.full_clean()
2852
p1.save()
2953

54+
p2.full_clean()
55+
p2.save()
56+
3057

3158
class Migration(migrations.Migration):
3259

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Generated by Django 4.2.17 on 2025-09-05 19:42
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("api_app", "0071_delete_last_elastic_report"),
11+
("analyzers_manager", "0164_update_capa"),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name="AnalyzerRulesFileVersion",
17+
fields=[
18+
(
19+
"id",
20+
models.BigAutoField(
21+
auto_created=True,
22+
primary_key=True,
23+
serialize=False,
24+
verbose_name="ID",
25+
),
26+
),
27+
(
28+
"last_downloaded_version",
29+
models.CharField(max_length=50, null=True, blank=True),
30+
),
31+
("download_url", models.URLField(null=True, blank=True)),
32+
("downloaded_at", models.DateTimeField(auto_now_add=True)),
33+
(
34+
"python_module",
35+
models.ForeignKey(
36+
on_delete=django.db.models.deletion.PROTECT,
37+
related_name="rules_version",
38+
to="api_app.pythonmodule",
39+
),
40+
),
41+
],
42+
),
43+
]

api_app/analyzers_manager/models.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,3 +350,13 @@ def plugin_type(cls) -> str:
350350
@property
351351
def config_exception(cls):
352352
return AnalyzerConfigurationException
353+
354+
355+
class AnalyzerRulesFileVersion(models.Model):
356+
last_downloaded_version = models.CharField(max_length=50, blank=True, null=True)
357+
download_url = models.URLField(max_length=200, blank=True, null=True)
358+
downloaded_at = models.DateTimeField(auto_now_add=True)
359+
360+
python_module = models.ForeignKey(
361+
PythonModule, on_delete=models.PROTECT, related_name="rules_version"
362+
)

requirements/project-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ wad==0.4.6
9191
debloat==1.6.4
9292
phonenumbers==9.0.3
9393
die-python==0.4.0
94-
# guarddog==2.1.0 # version greater than 2.1.0 raises dependency conflicts. Commenting this out due to dependency conflict.
94+
# guarddog==2.1.0 # version greater than 2.1.0 raises dependency conflicts. Commenting this out due to dependency conflicts.
9595
jbxapi==3.23.0
9696
flare-floss==3.1.1
9797
flare-capa==9.2.1

0 commit comments

Comments
 (0)