44import json
55import logging
66import os
7+ import shutil
78import subprocess
89from shlex import quote
910from zipfile import ZipFile
1011
1112import requests
1213from django .conf import settings
14+ from django .utils import timezone
1315
1416from api_app .analyzers_manager .classes import FileAnalyzer
1517from api_app .analyzers_manager .exceptions import AnalyzerRunException
18+ from api_app .analyzers_manager .models import AnalyzerRulesFileVersion , PythonModule
1619from tests .mock_utils import if_mock_connections , patch
1720
1821logger = 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
0 commit comments