4
4
import json
5
5
import logging
6
6
import os
7
+ import shutil
7
8
import subprocess
8
9
from shlex import quote
9
10
from zipfile import ZipFile
10
11
11
12
import requests
12
13
from django .conf import settings
14
+ from django .utils import timezone
13
15
14
16
from api_app .analyzers_manager .classes import FileAnalyzer
15
17
from api_app .analyzers_manager .exceptions import AnalyzerRunException
18
+ from api_app .analyzers_manager .models import AnalyzerRulesFileVersion , PythonModule
16
19
from tests .mock_utils import if_mock_connections , patch
17
20
18
21
logger = logging .getLogger (__name__ )
@@ -28,9 +31,44 @@ class CapaInfo(FileAnalyzer):
28
31
shellcode : bool
29
32
arch : str
30
33
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
31
46
32
47
@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 ):
34
72
logger .info (f"Extracting rules at { RULES_LOCATION } " )
35
73
with ZipFile (RULES_FILE , mode = "r" ) as archive :
36
74
archive .extractall (
@@ -41,31 +79,46 @@ def _unzip(cls):
41
79
@classmethod
42
80
def _download_rules (cls , latest_version : str ):
43
81
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 } " )
46
88
47
89
file_to_download = latest_version + ".zip"
48
90
file_url = RULES_URL + file_to_download
49
91
try :
50
92
51
93
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
+ )
53
97
with open (RULES_FILE , mode = "wb+" ) as file :
54
98
for chunk in response .iter_content (chunk_size = 10 * 1024 ):
55
99
file .write (chunk )
56
100
101
+ cls ._update_rules_file_version (latest_version , file_url )
102
+ logger .info (f"Bumped up version number in db to { latest_version } " )
103
+
57
104
except Exception as e :
58
105
logger .error (f"Failed to download rules with error: { e } " )
59
106
raise AnalyzerRunException ("Failed to download rules" )
60
107
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
+ )
62
111
63
112
@classmethod
64
113
def _download_signatures (cls ) -> None :
65
114
logger .info (f"Downloading signatures at { SIGNATURE_LOCATION } now" )
66
115
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 } " )
69
122
70
123
signatures_url = "https://api.github.com/repos/mandiant/capa/contents/sigs"
71
124
try :
@@ -77,28 +130,29 @@ def _download_signatures(cls) -> None:
77
130
filename = signature ["name" ]
78
131
download_url = signature ["download_url" ]
79
132
133
+ signature_file_path = os .path .join (SIGNATURE_LOCATION , filename )
134
+
80
135
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 :
82
137
for chunk in sig_content .iter_content (chunk_size = 10 * 1024 ):
83
138
file .write (chunk )
84
139
85
140
except Exception as e :
86
141
logger .error (f"Failed to download signature: { e } " )
87
142
raise AnalyzerRunException ("Failed to update signatures" )
88
- logger .info ("Successfully updated singatures " )
143
+ logger .info ("Successfully updated signatures " )
89
144
90
145
@classmethod
91
146
def update (cls ) -> bool :
92
147
try :
93
- logger .info ("Updating capa rules and signatures " )
148
+ logger .info ("Updating capa rules" )
94
149
response = requests .get (
95
150
"https://api.github.com/repos/mandiant/capa-rules/releases/latest"
96
151
)
97
152
latest_version = response .json ()["tag_name" ]
98
153
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" )
102
156
103
157
return True
104
158
@@ -109,16 +163,22 @@ def update(cls) -> bool:
109
163
110
164
def run (self ):
111
165
try :
112
- if (
113
- not (
114
- os .path .isdir (RULES_LOCATION ) and os .path .isdir (SIGNATURE_LOCATION )
115
- )
116
- and not self .update ()
117
- ):
118
166
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" )
122
182
123
183
command : list [str ] = ["/usr/local/bin/capa" , "--quiet" , "--json" ]
124
184
shell_code_arch = "sc64" if self .arch == "64" else "sc32"
@@ -136,7 +196,9 @@ def run(self):
136
196
137
197
command .append (quote (self .filepath ))
138
198
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
+ )
140
202
141
203
process : subprocess .CompletedProcess = subprocess .run (
142
204
command ,
@@ -147,13 +209,20 @@ def run(self):
147
209
)
148
210
149
211
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
+ )
151
218
152
219
except subprocess .CalledProcessError as e :
153
220
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
+ )
155
224
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 } "
157
226
)
158
227
159
228
return result
0 commit comments