def test_run(tmp_path: Path) -> None: tool = Flake8Tool( context_for(tmp_path, Flake8Tool.TOOL_ID, SIMPLE_INTEGRATION_PATH)) tool.setup() violations = tool.results(SIMPLE_TARGETS) expectation = [ Violation( tool_id="flake8", check_id="parse-error", path="foo.py", line=5, column=13, message="SyntaxError: invalid syntax", severity=2, syntactic_context="def broken(x)", filtered=None, link="", ), Violation( tool_id="flake8", check_id="indentation-error", path="foo.py", line=6, column=5, message="unexpected indentation", severity=2, syntactic_context="return x", filtered=None, link= "https://pycodestyle.readthedocs.io/en/latest/intro.html#error-codes", ), ] assert violations == expectation
def test_parse() -> None: with (THIS_PATH / "eslint_violation_simple.json").open() as json_file: data = json_file.read() result = EslintParser(BASE_PATH).parse(json.loads(data)) expectation = [ Violation( tool_id="r2c.eslint", check_id="no-console", path="tests/integration/simple/init.js", line=0, column=0, message="Unexpected console statement.", severity=1, syntactic_context="console.log(3)", ), Violation( tool_id="r2c.eslint", check_id="semi", path="tests/integration/simple/init.js", line=0, column=0, message="Missing semicolon.", severity=2, syntactic_context="console.log(3)", ), ] assert result == expectation
def test_run(tmp_path_factory: tmp_path_factory) -> None: base_path = BASE_PATH / "tests/integration/simple" tool = EslintTool( context_for(tmp_path_factory, EslintTool.ESLINT_TOOL_ID, base_path)) tool.setup() try: violations = tool.results() except subprocess.CalledProcessError as e: print(e.stderr) raise e expectation = [ Violation( tool_id="r2c.eslint", check_id="no-console", path="init.js", line=0, column=0, message="Unexpected console statement.", severity=1, syntactic_context="console.log(3)", ), Violation( tool_id="r2c.eslint", check_id="semi", path="init.js", line=0, column=0, message="Missing semicolon.", severity=2, syntactic_context="console.log(3)", ), ] assert violations == expectation
def test_run(tmp_path: Path) -> None: tool = EslintTool( context_for(tmp_path, EslintTool.ESLINT_TOOL_ID, SIMPLE_INTEGRATION_PATH)) tool.setup() try: violations = tool.results(SIMPLE_TARGETS) except subprocess.CalledProcessError as e: print(e.stderr) raise e expectation = [ Violation( tool_id="eslint", check_id="no-console", path="init.js", line=0, column=0, message="Unexpected console statement.", severity=1, syntactic_context="console.log(3)", ), Violation( tool_id="eslint", check_id="semi", path="init.js", line=0, column=0, message="Missing semicolon.", severity=2, syntactic_context="console.log(3)", ), ] assert violations == expectation
def test_run_flask_violations(tmp_path: Path) -> None: base_path = BASE_PATH / "tests/integration/requests" tool = RequestsTool(context_for(tmp_path, RequestsTool.TOOL_ID, base_path)) tool.setup() violations = tool.results([base_path / "bad.py"]) expectation = [ Violation( tool_id="r2c.requests", check_id="use-timeout", path="bad.py", line=3, column=5, message= "requests will hang forever without a timeout. Consider adding a timeout (recommended 10 sec).", severity=2, syntactic_context= "r = requests.get('http://MYURL.com', auth=('user', 'pass'))", filtered=None, link= "https://checks.bento.dev/en/latest/flake8-requests/use-timeout", ), Violation( tool_id="r2c.requests", check_id="no-auth-over-http", path="bad.py", line=3, column=5, message= "auth is possibly used over http://, which could expose credentials. possible_urls: ['http://MYURL.com']", severity=2, syntactic_context= "r = requests.get('http://MYURL.com', auth=('user', 'pass'))", filtered=None, link= "https://checks.bento.dev/en/latest/flake8-requests/no-auth-over-http", ), ] violations_important_info = set( map( lambda viol: (viol.tool_id, viol.check_id, viol.line, viol.column), violations, )) expectation_important_info = set( map( lambda viol: (viol.tool_id, viol.check_id, viol.line, viol.column), expectation, )) assert violations_important_info == expectation_important_info
def test_run(tmp_path_factory: tmp_path_factory) -> None: base_path = BASE_PATH / "tests/integration/py-only" tool = PyreTool(context_for(tmp_path_factory, PyreTool.TOOL_ID, base_path)) tool.setup() violations = tool.results() expectation = [ Violation( tool_id="r2c.pyre", check_id="6", path="bar.py", line=10, column=13, message= "Incompatible parameter type [6]: Expected `int` for 1st anonymous parameter to call `int.__radd__` but got `str`.", severity=2, syntactic_context= " x: int = cmd + 5 + os.getenv('doesnotexist')\n", link="https://pyre-check.org/docs/error-types.html", ), Violation( tool_id="r2c.pyre", check_id="6", path="bar.py", line=10, column=23, message= "Incompatible parameter type [6]: Expected `int` for 1st anonymous parameter to call `int.__add__` but got `typing.Optional[str]`.", severity=2, syntactic_context= " x: int = cmd + 5 + os.getenv('doesnotexist')\n", link="https://pyre-check.org/docs/error-types.html", ), Violation( tool_id="r2c.pyre", check_id="7", path="bar.py", line=11, column=4, message= "Incompatible return type [7]: Expected `str` but got `None`.", severity=2, syntactic_context=" return None\n", link="https://pyre-check.org/docs/error-types.html", ), ] assert set(violations) == set(expectation)
def test_run(tmp_path: Path) -> None: tool = BanditTool( context_for(tmp_path, BanditTool.TOOL_ID, SIMPLE_INTEGRATION_PATH)) tool.setup() violations = tool.results(SIMPLE_TARGETS) expectation = [ Violation( check_id="error", tool_id=BanditTool.TOOL_ID, path="foo.py", line=0, column=0, message="syntax error while parsing AST from file", severity=4, syntactic_context="", link=None, ), Violation( check_id="import-subprocess", tool_id=BanditTool.TOOL_ID, path="bar.py", line=1, column=0, message= "Consider possible security implications associated with subprocess module.", severity=1, syntactic_context=" import subprocess", link= "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html#b404-import-subprocess", ), Violation( check_id="subprocess-popen-with-shell-equals-true", tool_id=BanditTool.TOOL_ID, path="bar.py", line=4, column=0, message= "subprocess call with shell=True identified, security issue.", severity=3, syntactic_context= ' subprocess.run(f"bash -c {cmd}", shell=True)', link= "https://bandit.readthedocs.io/en/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html", ), ] assert violations == expectation
def to_violation( self, result: Dict[str, Any], message: Dict[str, Any] ) -> Violation: path = self.trim_base(result["filePath"]) startLine = message["line"] endLine = message.get("endLine", startLine) source = result["source"][startLine - 1 : endLine] # line numbers are 1-indexed check_id = message.get("ruleId", None) if check_id: link = self.to_link(check_id) else: check_id = "error" link = "" return Violation( tool_id=EslintTool.ESLINT_TOOL_ID, check_id=check_id, path=path, line=startLine, column=message["column"], message=message["message"], severity=message["severity"], syntactic_context="\n".join(source).rstrip(), link=link, )
def test_run_flask_violations(tmp_path_factory: tmp_path_factory) -> None: base_path = BASE_PATH / "tests/integration/requests" tool = RequestsTool( context_for(tmp_path_factory, RequestsTool.TOOL_ID, base_path)) tool.setup() violations = tool.results() expectation = [ Violation( tool_id="r2c.requests", check_id="no-auth-over-http", path="bad.py", line=2, column=5, message= "auth is possibly used over http://, which could expose credentials. possible_urls: ['http://MYURL.com']", severity=2, syntactic_context= "r = requests.get('http://MYURL.com', auth=('user', 'pass'))", filtered=None, link= "https://checks.bento.dev/en/latest/flake8-requests/r2c-requests-no-auth-over-http", ) ] assert violations == expectation
def test_run_flask_violations(tmp_path_factory: tmp_path_factory) -> None: base_path = BASE_PATH / "tests/integration/flask" tool = FlaskTool( context_for(tmp_path_factory, FlaskTool.TOOL_ID, base_path)) tool.setup() violations = tool.results() expectation = [ Violation( tool_id="r2c.flask", check_id="send-file-open", path="bad.py", line=4, column=1, message= "Passing a file-like object to flask.send_file without the mimetype or attachment_filename keyword arg will raise a ValueError. If you are sending a static file, pass in a string path to the file instead. Otherwise, specify a mimetype or attachment_filename in flask.send_file.", severity=2, syntactic_context='flask.send_file(open("file.txt"))', filtered=None, link= "https://checks.bento.dev/en/latest/flake8-flask/send-file-open", ) ] assert violations == expectation
def to_violation(self, result: Dict[str, Any]) -> Violation: start_line = result["line"] column = result["column"] check_id = f"SC{result['code']}" message = result["message"] path = result["file"] path = self.trim_base(path) level = result["level"] if level == "error": severity = 2 elif level == "warning": severity = 1 elif level == "info": severity = 0 elif level == "style": severity = 0 link = f"https://github.com/koalaman/shellcheck/wiki/{check_id}" line_of_code = (fetch_line_in_file(self.base_path / path, start_line) or "<no source found>") return Violation( tool_id=ShellcheckTool.TOOL_ID, check_id=check_id, path=path, line=start_line, column=column, message=message, severity=severity, syntactic_context=line_of_code, link=link, )
def to_violation(self, result: Dict[str, Any]) -> Violation: path = self.trim_base(result["path"]) start_line = result["start"]["line"] start_col = result["start"]["col"] message = result.get("extra", {}).get("message") check_id = result["check_id"] level = result.get("extra", {}).get("level") if level == "error": severity = 2 elif level == "warning": severity = 1 elif level == "info": severity = 0 elif level == "style": severity = 0 link = f"https://github.com/koalaman/shellcheck/wiki/{check_id}" line_of_code = (fetch_line_in_file(self.base_path / path, start_line) or "<no source found>") return Violation( tool_id=ShellcheckTool.tool_id(), check_id=check_id, path=path, line=start_line, column=start_col, message=message, severity=severity, syntactic_context=line_of_code, link=link, )
def test_run_flask_violations(tmp_path_factory: tmp_path_factory) -> None: base_path = BASE_PATH / "tests/integration/boto3" tool = Boto3Tool( context_for(tmp_path_factory, Boto3Tool.TOOL_ID, base_path)) tool.setup() violations = tool.results() expectation = [ Violation( tool_id="r2c.boto3", check_id="hardcoded-access-token", path="bad.py", line=4, column=11, message= "Hardcoded access token detected. Consider using a config file or environment variables.", severity=2, syntactic_context= "session = Session(aws_access_key_id='AKIA1235678901234567',", filtered=None, link= "https://checks.bento.dev/en/latest/flake8-boto3/hardcoded-access-token", ) ] assert violations == expectation
def test_parse() -> None: with (THIS_PATH / "boto3_violation_simple.json").open() as json_file: json = json_file.read() result = Boto3Parser(BASE_PATH).parse(json) expectation = [ Violation( tool_id="r2c.boto3", check_id="hardcoded-access-token", path="bad.py", line=4, column=1, message= "Hardcoded access token detected. Consider using a config file or environment variables.", severity=2, syntactic_context= "Session(aws_access_key_id='AKIA1235678901234567',", filtered=None, link= "https://checks.bento.dev/en/latest/flake8-boto3/hardcoded-access-token", ) ] assert result == expectation
def to_violation(self, result: Dict[str, Any]) -> Violation: start_line = int(result["line"]) column = int(result["column"]) check_id = result["rule_id"] message = result["details"] path = str(PurePath(result["file"]).relative_to(REMOTE_BASE_PATH)) level = result["severity"] severity = self.SEVERITIES.get(level, 0) link = result.get("cwe", {}).get("URL", "") line_of_code = (fetch_line_in_file(self.base_path / path, start_line) or "<no source found>") return Violation( tool_id=GosecTool.TOOL_ID, check_id=check_id, path=path, line=start_line, column=column, message=message, severity=severity, syntactic_context=line_of_code, link=link, )
def test_run(tmp_path: Path) -> None: tool = JinjalintTool( context_for(tmp_path, JinjalintTool.TOOL_ID, SIMPLE_INTEGRATION_PATH) ) tool.setup() violations = tool.results(SIMPLE_TARGETS) expectation = [ Violation( check_id="anchor-missing-noreferrer", tool_id=JinjalintTool.TOOL_ID, path="jinja-template.html", line=11, column=8, message="Pages opened with 'target=\"_blank\"' allow the new page to access the original's referrer. This can have privacy implications. Include 'rel=\"noreferrer\"' to prevent this.", severity=2, syntactic_context=' <a href="https://example.com" target="_blank">Test anchor</a>', link="https://bento.dev/checks/jinja/anchor-missing-noreferrer/", ), Violation( check_id="anchor-missing-noopener", tool_id=JinjalintTool.TOOL_ID, path="jinja-template.html", line=8, column=11, message="Pages opened with 'target=\"_blank\"' allow the new page to access the original's 'window.opener'. This can have security and performance implications. Include 'rel=\"noopener\"' to prevent this.", severity=2, syntactic_context=' <a href="https://example.com" target="_blank">Test anchor</a>', link="https://bento.dev/checks/jinja/anchor-missing-noopener/", ), Violation( check_id="form-missing-csrf-protection", tool_id=JinjalintTool.TOOL_ID, path="jinja-template.html", line=7, column=8, message="Flask apps using 'flask-wtf' require including a CSRF token in the HTML form. This check detects missing CSRF protection in HTML forms in Jinja templates.", severity=2, syntactic_context=' <form method="post">', link="https://bento.dev/checks/jinja/form-missing-csrf-protection/", ), ] assert set(violations) == set(expectation) # Avoid ordering constraints with set
def result_for(path: str) -> Violation: return Violation( tool_id="test", check_id="test", path=path, line=0, column=0, message="test", severity=2, syntactic_context="test", )
def test_run(tmp_path_factory: tmp_path_factory) -> None: base_path = BASE_PATH / "tests/integration/simple" tool = Flake8Tool( context_for(tmp_path_factory, Flake8Tool.TOOL_ID, base_path)) tool.setup() violations = tool.results() expectation = [ Violation( tool_id="r2c.flake8", check_id="E124", path="foo.py", line=2, column=0, message="closing bracket does not match visual indentation", severity=2, syntactic_context=" )", ), Violation( tool_id="r2c.flake8", check_id="E999", path="foo.py", line=5, column=0, message="SyntaxError: invalid syntax", severity=2, syntactic_context="def broken(x)", ), Violation( tool_id="r2c.flake8", check_id="E113", path="foo.py", line=6, column=0, message="unexpected indentation", severity=2, syntactic_context=" return x", ), ] assert violations == expectation
def __error_to_violation(self, error: Dict[str, Any]) -> Violation: return Violation( check_id="error", tool_id=BanditTool.TOOL_ID, path=self.trim_base(error["filename"]), severity=2, line=0, column=0, message=error["reason"], syntactic_context="", link=None, )
def test_typescript_run(tmp_path_factory: tmp_path_factory) -> None: base_path = BASE_PATH / "tests/integration/js-and-ts" tool = EslintTool( context_for(tmp_path_factory, EslintTool.ESLINT_TOOL_ID, base_path)) tool.setup() try: violations = tool.results(["foo.ts"]) except subprocess.CalledProcessError as e: print(e.stderr) raise e expectation = [ Violation( tool_id="r2c.eslint", check_id="@typescript-eslint/no-unused-vars", path="foo.ts", line=1, column=7, message="'user' is assigned a value but never used.", severity=1, syntactic_context="const user: int = 'Mom'", filtered=None, link= "https://eslint.org/docs/rules/@typescript-eslint/no-unused-vars", ), Violation( tool_id="r2c.eslint", check_id="semi", path="foo.ts", line=1, column=24, message="Missing semicolon.", severity=2, syntactic_context="const user: int = 'Mom'", filtered=None, link="https://eslint.org/docs/rules/semi", ), ] assert violations == expectation
def __result_to_violation(self, result: Dict[str, Any]) -> Violation: path = self.trim_base(result["filename"]) link = result.get("more_info", None) # Remove bandit line numbers, empty lines, and leading / trailing whitespace bandit_source = result["code"].rstrip() # Remove trailing whitespace test_id = result["test_id"] check_id = BANDIT_TO_BENTO.get(test_id, test_id) line_range = result["line_range"] def in_line_range(bandit_code_line: str) -> bool: # Check if string with format `3 def do_it(cmd: str) -> None:` # starts with line number that is within reported line_range # of finding for idx, ch in enumerate(bandit_code_line): if not ch.isdigit(): num = int(bandit_code_line[:idx]) return num in line_range return False # bandit might include extra lines before and after # a finding. Filter those out and filter out line numbers lines = [ s.lstrip(BanditParser.LINE_NO_CHARS).rstrip() for s in bandit_source.split("\n") if in_line_range(s) ] nonempty = [l for l in lines if l] source = "\n".join(nonempty) if source == "" and result["line_number"] != 0: source = (fetch_line_in_file(self.base_path / path, result["line_number"]) or "<no source found>") return Violation( check_id=check_id, tool_id=BanditTool.TOOL_ID, path=path, line=result["line_number"], column=0, message=result["issue_text"], severity=BanditParser.SEVERITY.get(result["issue_severity"], 1), syntactic_context=source, link=link, )
def to_violation(self, result: Dict[str, Any]) -> Violation: source = (result["physical_line"] or "").rstrip() # Remove trailing whitespace path = self.trim_base(result["filename"]) check_id = result["code"] return Violation( tool_id=self.tool().tool_id(), check_id=self.id_to_name(check_id), path=path, line=result["line_number"], column=result["column_number"], message=result["text"], severity=2, syntactic_context=source, link=self.id_to_link(check_id), )
def parse(self, tool_output: str) -> List[Violation]: results = json.loads(tool_output) violations = [ Violation( check_id=r["code"].replace(self.CHECK_PREFIX, ""), tool_id=JinjalintTool.TOOL_ID, path=self.trim_base(r["file_path"]), severity=JinjalintParser.SEVERITY["MEDIUM"], line=r["line"], column=r["column"], message=r["message"], syntactic_context=r.get("physical_line", ""), link=self._get_link(r["code"]), ) for r in results ] return violations
def to_violation(self, result: Dict[str, Any]) -> Violation: path = self.trim_base(result["path"]) abspath = self.base_path / path check_id = str(result["code"]) line = result["line"] return Violation( tool_id=PyreTool.TOOL_ID, check_id=check_id, path=path, line=line, column=result["column"], message=result["description"], severity=2, syntactic_context=fetch_line_in_file(abspath, line) or "<no source found>", link="https://pyre-check.org/docs/error-types.html", )
def to_violation(self, result: Dict[str, Any]) -> Violation: start_line = result["line"] column = result["column"] check_id = result["code"] message = result["message"] path = result["file"] path = self.trim_base(path) level = result["level"] if level == "error": severity = 2 elif level == "warning": severity = 1 elif level == "info": severity = 0 elif level == "style": severity = 0 if "DL" in check_id or check_id in ["SC2046", "SC2086"]: link = f"https://github.com/hadolint/hadolint/wiki/{check_id}" elif "SC" in check_id: link = f"https://github.com/koalaman/shellcheck/wiki/{check_id}" else: link = "" line_of_code = (fetch_line_in_file(self.base_path / path, start_line) or "<no source found>") if check_id == "DL1000": message = "Dockerfile parse error. Invalid docker instruction." return Violation( tool_id=HadolintTool.TOOL_ID, check_id=check_id, path=path, line=start_line, column=column, message=message, severity=severity, syntactic_context=line_of_code, link=link, )
def parse(self, tool_output: str) -> List[Violation]: results = json.loads(tool_output) violations = [ Violation( check_id=DLINT_TO_BENTO[result["code"]], tool_id=DlintTool.TOOL_ID, path=self.trim_base(filename), severity=DlintParser.SEVERITY["MEDIUM"], line=result["line_number"], column=result["column_number"], message=result["text"], syntactic_context=(result["physical_line"] or "").rstrip(), link=self._get_link(result["code"]), ) for filename, file_results in results.items() for result in file_results ] return violations
def test_run(tmp_path: Path) -> None: tool = DlintTool(context_for(tmp_path, DlintTool.TOOL_ID, SIMPLE_INTEGRATION_PATH)) tool.setup() violations = tool.results(SIMPLE_TARGETS) expectation = [ Violation( check_id="regular-expression-catastrophic-backtracking", tool_id=DlintTool.TOOL_ID, path="baz.py", line=4, column=0, message='catastrophic "re" usage - denial-of-service possible', severity=2, syntactic_context="re.search(r'(a+)+b', 'TEST')", link="https://github.com/dlint-py/dlint/blob/master/docs/linters/DUO138.md", ) ] assert violations == expectation
def to_violation(self, output_rule: Dict[str, Any]) -> Violation: output = output_rule["output"] check_id = output_rule["id"] message = output_rule.get("message") parts = output.split(":") path = parts[0] path = self.trim_base(path) line_no = int(parts[1]) code_snippet = ":".join(parts[2:]) return Violation( tool_id=GrepTool.TOOL_ID, check_id=check_id, path=path, line=line_no, column=1, message=message or code_snippet, severity=2, syntactic_context=code_snippet or "<no context>", )
def test_run(tmp_path: Path) -> None: base_path = BASE_PATH / "tests" / "integration" / "go" tool = GosecTool(context_for(tmp_path, GosecTool.tool_id(), base_path)) tool.setup() violations = tool.results([base_path / "bad.go"]) assert violations == [ Violation( tool_id="gosec", check_id="G101", path="bad.go", line=7, column=2, message="Potential hardcoded credentials", severity=2, syntactic_context= 'password := "******"\n', filtered=None, link="https://cwe.mitre.org/data/definitions/798.html", ) ]
def test_parse() -> None: with (THIS_PATH / "flake8_violation_simple.json").open() as json_file: json = json_file.read() result = Flake8Parser(BASE_PATH).parse(json) expectation = [ Violation( tool_id="flake8", check_id="E124", path="foo.py", line=2, column=0, message="closing bracket does not match visual indentation", severity=2, syntactic_context=" )", ) ] assert result == expectation