|
28 | 28 | #include <string.h> |
29 | 29 | #include <signal.h> |
30 | 30 | #include <locale.h> |
| 31 | +#include <json.h> |
31 | 32 |
|
32 | 33 | #ifdef HAVE_UNISTD_H |
33 | 34 | #include <unistd.h> |
@@ -73,13 +74,251 @@ static void loggBytes(uint64_t bytes) |
73 | 74 | } |
74 | 75 | } |
75 | 76 |
|
| 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 | + |
76 | 315 | int main(int argc, char **argv) |
77 | 316 | { |
78 | 317 | int ds, dms, ret; |
79 | 318 | struct timeval t1, t2; |
80 | 319 | time_t date_start, date_end; |
81 | 320 |
|
82 | | - char buffer[26]; |
| 321 | + char buffer[26] = {0}; |
83 | 322 | #ifdef _WIN32 |
84 | 323 | SetConsoleOutputCP(CP_UTF8); |
85 | 324 | #else /* !_WIN32 */ |
@@ -237,6 +476,10 @@ int main(int argc, char **argv) |
237 | 476 | } |
238 | 477 | strftime(buffer, sizeof(buffer), "%Y:%m:%d %H:%M:%S", &tmp); |
239 | 478 | 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 | + } |
240 | 483 | } |
241 | 484 |
|
242 | 485 | optfree(opts); |
@@ -282,6 +525,7 @@ void help(void) |
282 | 525 | mprintf(LOGG_INFO, " --official-db-only[=yes/no(*)] Only load official signatures.\n"); |
283 | 526 | mprintf(LOGG_INFO, " --fail-if-cvd-older-than=days Return with a nonzero error code if virus database outdated.\n"); |
284 | 527 | 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"); |
285 | 529 | mprintf(LOGG_INFO, " --recursive[=yes/no(*)] -r Scan subdirectories recursively.\n"); |
286 | 530 | mprintf(LOGG_INFO, " --allmatch[=yes/no(*)] -z Continue scanning within file after finding a match.\n"); |
287 | 531 | mprintf(LOGG_INFO, " --cross-fs[=yes(*)/no] Scan files and directories on other filesystems.\n"); |
|
0 commit comments