diff --git a/flawfinder.py b/flawfinder.py
index 3c72a3d..b3d001a 100644
--- a/flawfinder.py
+++ b/flawfinder.py
@@ -54,6 +54,7 @@ import time
import csv # To support generating CSV format
import hashlib
# import formatter
+from sariflogger import SarifLogger
version = "2.0.15"
@@ -87,6 +88,7 @@ output_format = 0 # 0 = normal, 1 = html.
single_line = 0 # 1 = singleline (can 't be 0 if html)
csv_output = 0 # 1 = Generate CSV
csv_writer = None
+sarif_output = 0 # 1 = Generate SARIF report
omit_time = 0 # 1 = omit time-to-run (needed for testing)
required_regex = None # If non-None, regex that must be met to report
required_regex_compiled = None
@@ -437,6 +439,8 @@ class Hit(object):
if csv_output:
self.show_csv()
return
+ if sarif_output:
+ return
if output_format:
print("
", end='')
sys.stdout.write(h(self.filename))
@@ -1790,6 +1794,8 @@ def display_header():
'Suggestion', 'Note', 'CWEs', 'Context', 'Fingerprint', 'ToolVersion', 'RuleId', 'HelpUri'
])
return
+ if sarif_output:
+ return
if not showheading:
return
if not displayed_header:
@@ -2044,7 +2050,7 @@ flawfinder [--help | -h] [--version] [--listrules]
def process_options():
global show_context, show_inputs, allowlink, skipdotdir, omit_time
global output_format, minimum_level, show_immediately, single_line
- global csv_output, csv_writer
+ global csv_output, csv_writer, sarif_output
global error_level
global required_regex, required_regex_compiled
global falsepositive
@@ -2058,7 +2064,7 @@ def process_options():
"falsepositive", "falsepositives", "columns", "listrules",
"omittime", "allowlink", "patch=", "followdotdir", "neverignore",
"regex=", "quiet", "dataonly", "html", "singleline", "csv",
- "error-level=",
+ "error-level=", "sarif",
"loadhitlist=", "savehitlist=", "diffhitlist=", "version", "help"
])
for (opt, value) in optlist:
@@ -2097,6 +2103,10 @@ def process_options():
quiet = 1
showheading = 0
csv_writer = csv.writer(sys.stdout)
+ elif opt == "--sarif":
+ sarif_output = 1
+ quiet = 1
+ showheading = 0
elif opt == "--error-level":
error_level = int(value)
elif opt == "--immediate" or opt == "-i":
@@ -2349,7 +2359,10 @@ def flawfind():
display_header()
initialize_ruleset()
if process_files():
- show_final_results()
+ if sarif_output:
+ print(SarifLogger(hitlist).output_sarif())
+ else:
+ show_final_results()
save_if_desired()
return 1 if error_level_exceeded else 0
diff --git a/sariflogger.py b/sariflogger.py
new file mode 100644
index 0000000..6568f31
--- /dev/null
+++ b/sariflogger.py
@@ -0,0 +1,164 @@
+import json
+import re
+
+def to_json(o):
+ return json.dumps(o, default=lambda o: o.__dict__, sort_keys=False, indent=2)
+
+class SarifLogger(object):
+ _hitlist = None
+ TOOL_NAME = "Flawfinder"
+ TOOL_URL = "https://dwheeler.com/flawfinder/"
+ TOOL_VERSION = "2.0.15"
+ URI_BASE_ID = "SRCROOT"
+ SARIF_SCHEMA = "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json"
+ SARIF_SCHEMA_VERSION = "2.1.0"
+ CWE_TAXONOMY_NAME = "CWE"
+ CWE_TAXONOMY_URI = "https://raw.githubusercontent.com/sarif-standard/taxonomies/main/CWE_v4.4.sarif"
+ CWE_TAXONOMY_GUID = "FFC64C90-42B6-44CE-8BEB-F6B7DAE649E5"
+
+ def __init__ (self, hits):
+ self._hitlist = hits
+
+ def output_sarif(self):
+ tool = {
+ "driver": {
+ "name": self.TOOL_NAME,
+ "version": self.TOOL_VERSION,
+ "informationUri": self.TOOL_URL,
+ "rules": self._extract_rules(self._hitlist),
+ "supportedTaxonomies": [{
+ "name": self.CWE_TAXONOMY_NAME,
+ "guid": self.CWE_TAXONOMY_GUID,
+ }],
+ }
+ }
+
+ runs = [{
+ "tool": tool,
+ "columnKind": "utf16CodeUnits",
+ "results": self._extract_results(self._hitlist),
+ "externalPropertyFileReferences": {
+ "taxonomies": [{
+ "location": {
+ "uri": self.CWE_TAXONOMY_URI,
+ },
+ "guid": self.CWE_TAXONOMY_GUID,
+ }],
+ },
+ }]
+
+ report = {
+ "$schema": self.SARIF_SCHEMA,
+ "version": self.SARIF_SCHEMA_VERSION,
+ "runs": runs,
+ }
+
+ jsonstr = to_json(report)
+ return jsonstr
+
+ def _extract_rules(self, hitlist):
+ rules = {}
+ for hit in hitlist:
+ if not hit.ruleid in rules:
+ rules[hit.ruleid] = self._to_sarif_rule(hit)
+ return list(rules.values())
+
+ def _extract_results(self, hitlist):
+ results = []
+ for hit in hitlist:
+ results.append(self._to_sarif_result(hit))
+ return results
+
+ def _to_sarif_rule(self, hit):
+ return {
+ "id": hit.ruleid,
+ "name": "{0}/{1}".format(hit.category, hit.name),
+ "shortDescription": {
+ "text": self._append_period(hit.warning),
+ },
+ "defaultConfiguration": {
+ "level": self._to_sarif_level(hit.defaultlevel),
+ },
+ "helpUri": hit.helpuri(),
+ "relationships": self._extract_relationships(hit.cwes()),
+ }
+
+ def _to_sarif_result(self, hit):
+ return {
+ "ruleId": hit.ruleid,
+ "level": self._to_sarif_level(hit.level),
+ "message": {
+ "text": self._append_period("{0}/{1}:{2}".format(hit.category, hit.name, hit.warning)),
+ },
+ "locations": [{
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": self._to_uri_path(hit.filename),
+ "uriBaseId": self.URI_BASE_ID,
+ },
+ "region": {
+ "startLine": hit.line,
+ "startColumn": hit.column,
+ "endColumn": len(hit.context_text) + 1,
+ "snippet": {
+ "text": hit.context_text,
+ }
+ }
+ }
+ }],
+ "fingerprints": {
+ "contextHash/v1": hit.fingerprint()
+ },
+ "rank": self._to_sarif_rank(hit.level),
+ }
+
+ def _extract_relationships(self, cwestring):
+ # example cwe string "CWE-119!/ CWE-120", "CWE-829, CWE-20"
+ relationships = []
+ for cwe in re.split(',|/',cwestring):
+ cwestr = cwe.strip()
+ if cwestr:
+ relationship = {
+ "target": {
+ "id": int(cwestr.replace("CWE-", "").replace("!", "")),
+ "toolComponent": {
+ "name": self.CWE_TAXONOMY_NAME,
+ "guid": self.CWE_TAXONOMY_GUID,
+ },
+ },
+ "kinds": [
+ "relevant" if cwestr[-1] != '!' else "incomparable"
+ ],
+ }
+ relationships.append(relationship)
+ return relationships
+
+ @staticmethod
+ def _to_sarif_level(level):
+ # level 4 & 5
+ if level >= 4:
+ return "error"
+ # level 3
+ if level == 3:
+ return "warning"
+ # level 0 1 2
+ return "note"
+
+ @staticmethod
+ def _to_sarif_rank(level):
+ #SARIF rank FF Level SARIF level Default Viewer Action
+ #0.0 0 note Does not display by default
+ #0.2 1 note Does not display by default
+ #0.4 2 note Does not display by default
+ #0.6 3 warning Displays by default, does not break build / other processes
+ #0.8 4 error Displays by default, breaks build/ other processes
+ #1.0 5 error Displays by default, breaks build/ other processes
+ return level * 0.2
+
+ @staticmethod
+ def _to_uri_path(path):
+ return path.replace("\\", "/")
+
+ @staticmethod
+ def _append_period(text):
+ return text if text[-1] == '.' else text + "."