Skip to content

Commit 5b9df2f

Browse files
committed
Add a new playground to the docs
This change introduces a new playground in the docs where a user can test out Bandit right within their browser. This code uses PyScript (and sphinx-pyscript) to generate an editor window on a sphinx page in our docs. When the user clicks the play button, it runs Bandit against the example code they have provided. If Bandit finds issues it renders them in a box on the same page under the editable code. The editor windows by default includes an example of correct and incorrect usage of ssl context. Signed-off-by: Eric Brown <[email protected]>
1 parent bcb6648 commit 5b9df2f

File tree

6 files changed

+204
-1
lines changed

6 files changed

+204
-1
lines changed

doc/requirements.txt

+4
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@
44
sphinx>=4.0.0 # BSD
55
sphinx-rtd-theme>=0.3.0
66
sphinx-copybutton
7+
8+
# FIXME: remove index url once offical sphinx-pyscript 0.2.0
9+
--extra-index-url https://test.pypi.org/simple/
10+
sphinx-pyscript-temp == 0.2.0

doc/source/_static/css/custom.css

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.issue-block {
2+
border: 1px solid LightGray;
3+
padding-left: .5em;
4+
padding-top: .5em;
5+
padding-bottom: .5em;
6+
margin-bottom: .5em;
7+
}
8+
9+
.issue-sev-high {
10+
background-color: Pink;
11+
}
12+
13+
.issue-sev-medium {
14+
background-color: NavajoWhite;
15+
}
16+
17+
.issue-sev-low {
18+
background-color: LightCyan;
19+
}

doc/source/conf.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"sphinx.ext.coverage",
1515
"sphinx.ext.viewcode",
1616
"sphinx_copybutton",
17+
"sphinx_pyscript_temp", # FIXME: replace with sphinx_pyscript
1718
]
1819

1920
# autodoc generation is a bit aggressive and a nuisance when doing heavy
@@ -63,8 +64,20 @@
6364
# Sphinx are currently 'default' and 'sphinxdoc'.
6465
# html_theme_path = ["."]
6566
html_theme = "sphinx_rtd_theme"
66-
# html_static_path = ['static']
67+
# These folders are copied to the documentation's HTML output
68+
html_static_path = ["_static"]
69+
# These paths are either relative to html_static_path
70+
# or fully qualified paths (eg. https://...)
71+
html_css_files = [
72+
"css/custom.css",
73+
# FIXME: setting priority here overrides the outdated pyscript css
74+
("https://pyscript.net/releases/2024.9.2/core.css", {"priority": 500}),
75+
]
6776
html_theme_options = {}
77+
html_js_files = [
78+
# FIXME: setting priority here overrides the outdated pyscript js
79+
("https://pyscript.net/releases/2024.9.2/core.js", {"type": "module", "priority": 500}),
80+
]
6881

6982
# Output file base name for HTML help builder.
7083
htmlhelp_basename = f"{project}doc"

doc/source/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Using and Extending Bandit
1717
blacklists/index
1818
formatters/index
1919
faq
20+
playground
2021

2122
Contributing
2223
============

doc/source/playground.py

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import io
2+
import logging
3+
import tokenize
4+
5+
from bandit.core import config
6+
from bandit.core import docs_utils
7+
from bandit.core import manager
8+
from bandit.core import meta_ast
9+
from bandit.core import metrics
10+
from bandit.core import node_visitor
11+
from bandit.core import test_set
12+
from pyscript import document
13+
14+
15+
# Disable noisy output from Bandit getting rendered to page
16+
logging.basicConfig(level=logging.ERROR)
17+
18+
ISSUE_BLOCK = """
19+
<div id="issue-{issue_no}">
20+
<div class="issue-block {issue_class}">
21+
<b>[{test_id}:{test_name}]</b> {test_text}<br>
22+
<b>Severity: </b>{severity}<br>
23+
<b>Confidence: </b>{confidence}<br>
24+
<b>CWE: </b><a href="{cwe_link}" target="_blank">{cwe}</a><br>
25+
<b>More info: </b><a href="{url}" target="_blank">{url}</a><br>
26+
<b>Location: </b>&lt;stdin&gt;:{line_number}:{col_offset}<br>
27+
</div>
28+
</div>
29+
"""
30+
31+
MESSAGE_BLOCK = """
32+
<div id="no-issues">
33+
<div class="issue-block">
34+
<b>{message}</b><br>
35+
</div>
36+
</div>
37+
"""
38+
39+
output_element = document.getElementById("output")
40+
41+
42+
def run_analysis(code):
43+
issue_metrics = metrics.Metrics()
44+
scores = []
45+
skipped = []
46+
filename = "<stdin>"
47+
48+
# Clear previous output
49+
output_element.innerHTML = ""
50+
51+
try:
52+
fobj = io.BytesIO(code)
53+
issue_metrics.begin(filename)
54+
data = fobj.read()
55+
lines = data.splitlines()
56+
issue_metrics.count_locs(lines)
57+
nosec_lines = {}
58+
59+
try:
60+
fobj.seek(0)
61+
tokens = tokenize.tokenize(fobj.readline)
62+
for toktype, tokval, (lineno, _), _, _ in tokens:
63+
if toktype == tokenize.COMMENT:
64+
nosec_lines[lineno] = manager._parse_nosec_comment(tokval)
65+
except tokenize.TokenError:
66+
pass
67+
68+
visitor = node_visitor.BanditNodeVisitor(
69+
filename,
70+
fobj,
71+
metaast=meta_ast.BanditMetaAst(),
72+
testset=test_set.BanditTestSet(
73+
config.BanditConfig(),
74+
profile={
75+
"include": [],
76+
"exclude": ["B613"], # FIXME: issue #1182
77+
},
78+
),
79+
debug=False,
80+
nosec_lines=nosec_lines,
81+
metrics=issue_metrics,
82+
)
83+
score = visitor.process(code)
84+
scores.append(score)
85+
issue_metrics.count_issues([score])
86+
87+
for index, issue in enumerate(visitor.tester.results):
88+
url = docs_utils.get_url(issue.test_id)
89+
output_element.innerHTML += ISSUE_BLOCK.format(
90+
issue_no=index,
91+
issue_class=f"issue-sev-{issue.severity.lower()}",
92+
test_name=issue.test,
93+
test_id=issue.test_id,
94+
test_text=issue.text,
95+
severity=issue.severity.capitalize(),
96+
confidence=issue.confidence.capitalize(),
97+
cwe=str(issue.cwe),
98+
cwe_link=issue.cwe.link(),
99+
url=url,
100+
line_number=issue.lineno,
101+
col_offset=issue.col_offset,
102+
)
103+
104+
if not visitor.tester.results:
105+
output_element.innerHTML += MESSAGE_BLOCK.format(
106+
message="No issues identified."
107+
)
108+
except SyntaxError:
109+
output_element.innerHTML += MESSAGE_BLOCK.format(
110+
message="Syntax error parsing code."
111+
)
112+
except Exception:
113+
output_element.innerHTML += MESSAGE_BLOCK.format(
114+
message="Exception scanning code."
115+
)
116+
117+
issue_metrics.aggregate()
118+
119+
120+
def handle_event(event):
121+
run_analysis(event.code.encode())
122+
123+
# prevent default execution
124+
return False
125+
126+
127+
editor = document.getElementById("editor")
128+
editor.handleEvent = handle_event

doc/source/playground.rst

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
Playground
2+
==========
3+
4+
Welcome to the Bandit Playground! This interactive web page allows you to
5+
experience the power of Bandit, a leading Static Application Security Testing
6+
(SAST) tool designed to help identify security issues in Python code. Bandit
7+
scans your code for potential vulnerabilities and provides detailed insights
8+
to help improve the overall security of your application. Whether you’re a
9+
security professional or a developer looking to secure your codebase, this
10+
playground offers a hands-on way to experiment with Bandit’s capabilities
11+
in real-time. Simply paste your Python code into the editor, run the scan,
12+
and explore the results instantly!
13+
14+
.. py-config::
15+
16+
splashscreen:
17+
autoclose: true
18+
packages:
19+
- bandit
20+
21+
.. raw:: html
22+
23+
<script type="py-editor" id="editor">
24+
import ssl
25+
26+
# Correct
27+
context = ssl.create_default_context()
28+
29+
# Incorrect: unverified context
30+
context = ssl._create_unverified_context()
31+
</script>
32+
33+
.. py-script::
34+
:file: playground.py
35+
36+
.. raw:: html
37+
38+
<div id="output"></div>

0 commit comments

Comments
 (0)