Skip to content

Commit 381fea8

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 381fea8

File tree

6 files changed

+206
-1
lines changed

6 files changed

+206
-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

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

doc/source/playground.rst

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
Bandit 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)