Skip to content

Commit cf04973

Browse files
committed
ADD: Minimal sarif output for clamscan
1 parent 1b55785 commit cf04973

4 files changed

Lines changed: 295 additions & 12 deletions

File tree

clamscan/clamscan.c

Lines changed: 245 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include <string.h>
2929
#include <signal.h>
3030
#include <locale.h>
31+
#include <json.h>
3132

3233
#ifdef HAVE_UNISTD_H
3334
#include <unistd.h>
@@ -73,13 +74,251 @@ static void loggBytes(uint64_t bytes)
7374
}
7475
}
7576

77+
static void generate_sarif_report(const char *filename, int argc, char **argv)
78+
{
79+
struct json_object *sarif = NULL;
80+
struct json_object *runs = NULL, *run = NULL, *tool = NULL, *driver = NULL;
81+
struct json_object *s_desc = NULL, *props_local = NULL, *results = NULL;
82+
83+
if (!filename) {
84+
mprintf(LOGG_ERROR, "SARIF filename is NULL\n");
85+
return;
86+
}
87+
88+
/* quick check that we can create/truncate the destination file */
89+
FILE *f = fopen(filename, "w");
90+
if (!f) {
91+
mprintf(LOGG_ERROR, "Failed to open SARIF report file for writing: %s\n", filename);
92+
return;
93+
}
94+
fclose(f);
95+
96+
sarif = json_object_new_object();
97+
if (!sarif) {
98+
mprintf(LOGG_ERROR, "Failed to allocate SARIF json object\n");
99+
return;
100+
}
101+
/*
102+
version:
103+
Schema:
104+
runs:
105+
- tool:
106+
driver:
107+
name:
108+
version:
109+
shortDescription:
110+
informationUri:
111+
properties:
112+
dbVersion:
113+
dbTime:
114+
- invocations:
115+
commandLine:
116+
arguments:
117+
- results:
118+
level:
119+
*/
120+
121+
json_object_object_add(sarif, "version", json_object_new_string("2.1.0"));
122+
json_object_object_add(sarif, "$schema", json_object_new_string(
123+
"https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json"));
124+
125+
runs = json_object_new_array();
126+
run = json_object_new_object();
127+
tool = json_object_new_object();
128+
driver = json_object_new_object();
129+
130+
if (!runs || !run || !tool || !driver) {
131+
mprintf(LOGG_ERROR, "Failed to allocate internal SARIF json objects\n");
132+
goto cleanup;
133+
}
134+
135+
json_object_object_add(driver, "name", json_object_new_string("ClamAV"));
136+
json_object_object_add(driver, "version", json_object_new_string(get_version() ? get_version() : "unknown"));
137+
s_desc = json_object_new_object();
138+
if (!s_desc) {
139+
mprintf(LOGG_ERROR, "Failed to allocate driver.shortDescription json object\n");
140+
goto cleanup;
141+
}
142+
json_object_object_add(s_desc, "text", json_object_new_string("ClamAV (clamscan) scan results"));
143+
json_object_object_add(driver, "shortDescription", s_desc);
144+
json_object_object_add(driver, "informationUri", json_object_new_string("https://www.clamav.net/"));
145+
146+
props_local = json_object_new_object();
147+
if (!props_local) {
148+
mprintf(LOGG_ERROR, "Failed to allocate run properties json object\n");
149+
goto cleanup;
150+
}
151+
152+
/* try to populate DB info using cl_get_db_build_info() */
153+
{
154+
unsigned int db_version = 0;
155+
time_t db_time = 0;
156+
char dbverstr[32] = {0};
157+
char db_timestr[32]= {0};
158+
159+
int got = cl_get_db_build_info(NULL, &db_version, &db_time);
160+
if (got > 0) {
161+
snprintf(dbverstr, sizeof(dbverstr), "%u", db_version);
162+
struct tm tm;
163+
#ifdef _WIN32
164+
gmtime_s(&tm, &db_time);
165+
#else
166+
gmtime_r(&db_time, &tm);
167+
#endif
168+
strftime(db_timestr, sizeof(db_timestr), "%Y-%m-%dT%H:%M:%SZ", &tm);
169+
json_object_object_add(props_local, "dbVersion", json_object_new_string(dbverstr));
170+
json_object_object_add(props_local, "dbTime", json_object_new_string(db_timestr));
171+
} else if (got == 0) {
172+
json_object_object_add(props_local, "dbVersion", json_object_new_string("unknown"));
173+
json_object_object_add(props_local, "dbTime", json_object_new_string("unknown"));
174+
} else {
175+
json_object_object_add(props_local, "dbVersion", json_object_new_string("error"));
176+
json_object_object_add(props_local, "dbTime", json_object_new_string("error"));
177+
}
178+
}
179+
180+
json_object_object_add(run, "properties", props_local);
181+
/* run now owns props_local */
182+
props_local = NULL;
183+
184+
json_object_object_add(tool, "driver", driver);
185+
/* tool now owns driver */
186+
driver = NULL;
187+
json_object_object_add(run, "tool", tool);
188+
/* run now owns tool */
189+
tool = NULL;
190+
json_object_array_add(runs, run);
191+
/* runs now owns run */
192+
json_object_object_add(sarif, "runs", runs);
193+
/* sarif now owns runs */
194+
195+
results = json_object_new_array();
196+
if (!results) {
197+
mprintf(LOGG_ERROR, "Failed to allocate results json array\n");
198+
goto cleanup;
199+
}
200+
json_object_object_add(run, "results", results);
201+
/* run now owns results */
202+
if (info.ifiles > 0) {
203+
/* if infected, add at least a minimal result entry */
204+
struct json_object *result = json_object_new_object();
205+
if (!result) {
206+
mprintf(LOGG_ERROR, "Failed to allocate a result json object\n");
207+
goto cleanup;
208+
}
209+
json_object_object_add(result, "level", json_object_new_string("error"));
210+
json_object_array_add(results, result);
211+
/* results owns result */
212+
result = NULL;
213+
/* TODO: add locations and messages for each infected file, probably
214+
could use ruleId as the virus signature identifier */
215+
/* Note: I don't think this is easily doable without some more
216+
substantial changes to the codebase since clamscan doesn't REALLY
217+
store findings. Quick and dirty would be to just monitor stderr/stdout,
218+
but having an actual datastructure storing the findings as it scans
219+
is probably better.*/
220+
}
221+
222+
/* Add invocation info: store argv as an arguments[] array and include
223+
* workingDirectory (uses getcwd to build a file:// URI). */
224+
{
225+
struct json_object *invocations = json_object_new_array();
226+
if (invocations) {
227+
struct json_object *invocation = json_object_new_object();
228+
if (invocation) {
229+
struct json_object *args_array = json_object_new_array();
230+
if (args_array) {
231+
int i = 0;
232+
for (i = 0; i < argc; ++i) {
233+
const char *a = argv[i] ? argv[i] : "";
234+
json_object_array_add(args_array, json_object_new_string(a));
235+
}
236+
json_object_object_add(invocation, "arguments", args_array);
237+
238+
/* workingDirectory: "uri": "file:///path/to/cwd" */
239+
char cwdbuf[PATH_MAX + 1] = {0};
240+
if (
241+
#ifdef _WIN32
242+
_getcwd(cwdbuf, sizeof(cwdbuf)) != NULL
243+
#else
244+
getcwd(cwdbuf, sizeof(cwdbuf)) != NULL
245+
#endif
246+
) {
247+
#ifdef _WIN32
248+
/* normalize backslashes in-place */
249+
for (char *p = cwdbuf; *p; ++p) if (*p == '\\') *p = '/';
250+
char uri_buf[PATH_MAX + 8];
251+
int rc = snprintf(uri_buf, sizeof(uri_buf), "file:///%s", cwdbuf);
252+
#else
253+
char uri_buf[PATH_MAX + 8];
254+
int rc = snprintf(uri_buf, sizeof(uri_buf), "file://%s", cwdbuf);
255+
#endif
256+
if (rc > 0 && rc < (int)sizeof(uri_buf)) {
257+
struct json_object *work_dir = json_object_new_object();
258+
if (work_dir) {
259+
struct json_object *uri = json_object_new_string(uri_buf);
260+
if (uri) {
261+
json_object_object_add(work_dir, "uri", uri);
262+
json_object_object_add(invocation, "workingDirectory", work_dir);
263+
} else {
264+
json_object_put(work_dir);
265+
}
266+
}
267+
}
268+
}
269+
270+
json_object_array_add(invocations, invocation);
271+
json_object_object_add(run, "invocations", invocations);
272+
} else {
273+
json_object_put(invocation);
274+
json_object_put(invocations);
275+
}
276+
} else {
277+
json_object_put(invocations);
278+
}
279+
}
280+
}
281+
282+
/* We've finished mutating run/runs/results; null local refs so cleanup
283+
* doesn't attempt to free attached objects twice (sarif owns them).
284+
*/
285+
run = NULL;
286+
results = NULL;
287+
runs = NULL;
288+
289+
/* Write the JSON to file (pretty-printed). */
290+
if (json_object_to_file_ext(filename, sarif, JSON_C_TO_STRING_PRETTY) != 0) {
291+
mprintf(LOGG_ERROR, "Failed to write SARIF report to %s\n", filename);
292+
goto cleanup;
293+
}
294+
295+
cleanup:
296+
if (sarif)
297+
json_object_put(sarif);
298+
/* free any partially-allocated objects that were never attached */
299+
if (runs)
300+
json_object_put(runs);
301+
if (run)
302+
json_object_put(run);
303+
if (tool)
304+
json_object_put(tool);
305+
if (driver)
306+
json_object_put(driver);
307+
if (s_desc)
308+
json_object_put(s_desc);
309+
if (props_local)
310+
json_object_put(props_local);
311+
if (results)
312+
json_object_put(results);
313+
}
314+
76315
int main(int argc, char **argv)
77316
{
78317
int ds, dms, ret;
79318
struct timeval t1, t2;
80319
time_t date_start, date_end;
81320

82-
char buffer[26];
321+
char buffer[26] = {0};
83322
#ifdef _WIN32
84323
SetConsoleOutputCP(CP_UTF8);
85324
#else /* !_WIN32 */
@@ -237,6 +476,10 @@ int main(int argc, char **argv)
237476
}
238477
strftime(buffer, sizeof(buffer), "%Y:%m:%d %H:%M:%S", &tmp);
239478
logg(LOGG_INFO, "End Date: %s\n", buffer);
479+
480+
if(optget(opts, "sarif")->enabled) {
481+
generate_sarif_report(optget(opts, "sarif")->strarg, argc, argv);
482+
}
240483
}
241484

242485
optfree(opts);
@@ -282,6 +525,7 @@ void help(void)
282525
mprintf(LOGG_INFO, " --official-db-only[=yes/no(*)] Only load official signatures.\n");
283526
mprintf(LOGG_INFO, " --fail-if-cvd-older-than=days Return with a nonzero error code if virus database outdated.\n");
284527
mprintf(LOGG_INFO, " --log=FILE -l FILE Save scan report to FILE.\n");
528+
mprintf(LOGG_INFO, " --sarif=FILE Save scan report to FILE in SARIF format.\n");
285529
mprintf(LOGG_INFO, " --recursive[=yes/no(*)] -r Scan subdirectories recursively.\n");
286530
mprintf(LOGG_INFO, " --allmatch[=yes/no(*)] -z Continue scanning within file after finding a match.\n");
287531
mprintf(LOGG_INFO, " --cross-fs[=yes(*)/no] Scan files and directories on other filesystems.\n");

common/misc.c

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -126,26 +126,55 @@ char *freshdbdir(void)
126126

127127
void print_version(const char *dbdir)
128128
{
129-
char *fdbdir = NULL, *path;
129+
unsigned int db_version = 0;
130+
time_t db_time = 0;
131+
int got = 0;
132+
133+
got = cl_get_db_build_info(dbdir, &db_version, &db_time);
134+
135+
if (got < 0) {
136+
/* error while trying to determine DB info */
137+
printf("ClamAV %s\n", get_version());
138+
return;
139+
}
140+
141+
if (got) {
142+
printf("ClamAV %s/%u/%s", get_version(), db_version, ctime(&db_time));
143+
} else {
144+
printf("ClamAV %s\n", get_version());
145+
}
146+
}
147+
148+
/*
149+
* cl_get_db_build_info - locate the freshest daily DB in the provided
150+
* directory (or the default directories if dbdir is NULL) and return
151+
* the database version and build time.
152+
*
153+
* Returns: 1 if db info was found and filled, 0 if no daily DB found,
154+
* and -1 on error (e.g. allocation failure).
155+
*/
156+
int cl_get_db_build_info(const char *dbdir, unsigned int *out_version, time_t *out_time)
157+
{
158+
char *fdbdir = NULL, *path = NULL;
130159
const char *pt;
131-
struct cl_cvd *daily;
132-
time_t db_time;
160+
struct cl_cvd *daily = NULL;
133161
unsigned int db_version = 0;
162+
time_t db_time = 0;
134163

135164
if (dbdir)
136165
pt = dbdir;
137166
else
138167
pt = fdbdir = freshdbdir();
139168

140169
if (!pt) {
141-
printf("ClamAV %s\n", get_version());
142-
return;
170+
return 0; /* nothing found */
143171
}
144172

145-
if (!(path = malloc(strlen(pt) + 11))) {
173+
path = malloc(strlen(pt) + 11);
174+
if (!path) {
146175
if (!dbdir)
147176
free(fdbdir);
148-
return;
177+
return -1;
149178
}
150179

151180
sprintf(path, "%s" PATHSEP "daily.cvd", pt);
@@ -170,16 +199,19 @@ void print_version(const char *dbdir)
170199
}
171200
}
172201

202+
free(path);
173203
if (!dbdir)
174204
free(fdbdir);
175205

176206
if (db_version) {
177-
printf("ClamAV %s/%u/%s", get_version(), db_version, ctime(&db_time));
178-
} else {
179-
printf("ClamAV %s\n", get_version());
207+
if (out_version)
208+
*out_version = db_version;
209+
if (out_time)
210+
*out_time = db_time;
211+
return 1;
180212
}
181213

182-
free(path);
214+
return 0;
183215
}
184216

185217
int check_flevel(void)

common/misc.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,9 @@ unsigned int countlines(const char *filename);
109109
/* Checks if a virus database file or directory is older than 'days'. */
110110
cl_error_t check_if_cvd_outdated(const char *path, long long days);
111111

112+
/* Retrieve DB build info: version and build time.
113+
* Returns 1 if found, 0 if no DB found, -1 on error.
114+
*/
115+
int cl_get_db_build_info(const char *dbdir, unsigned int *out_version, time_t *out_time);
116+
112117
#endif

common/optparser.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ const struct clam_option __clam_options[] = {
276276

277277
{"LogFile", "log", 'l', CLOPT_TYPE_STRING, NULL, -1, NULL, 0, OPT_CLAMD | OPT_MILTER | OPT_CLAMSCAN | OPT_CLAMDSCAN | OPT_CLAMONACC, "Save all reports to a log file.", "/tmp/clamav.log"},
278278

279+
{"Sarif", "sarif", 0, CLOPT_TYPE_STRING, NULL, -1, NULL, 0, OPT_CLAMD | OPT_MILTER | OPT_CLAMSCAN | OPT_CLAMDSCAN | OPT_CLAMONACC, "Produce a report in SARIF format.", "/tmp/clamav.sarif"},
280+
279281
{"LogFileUnlock", NULL, 0, CLOPT_TYPE_BOOL, MATCH_BOOL, 0, NULL, 0, OPT_CLAMD | OPT_MILTER, "By default the log file is locked for writing and only a single\ndaemon process can write to it. This option disables the lock.", "yes"},
280282

281283
{"LogFileMaxSize", NULL, 0, CLOPT_TYPE_SIZE, MATCH_SIZE, 1048576, NULL, 0, OPT_CLAMD | OPT_FRESHCLAM | OPT_MILTER, "Maximum size of the log file.\nValue of 0 disables the limit.", "5M"},

0 commit comments

Comments
 (0)