main.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. #
  2. # Copyright 2014 Hewlett-Packard Development Company, L.P.
  3. #
  4. # SPDX-License-Identifier: Apache-2.0
  5. """Bandit is a tool designed to find common security issues in Python code."""
  6. import argparse
  7. import fnmatch
  8. import logging
  9. import os
  10. import sys
  11. import textwrap
  12. import bandit
  13. from bandit.core import config as b_config
  14. from bandit.core import constants
  15. from bandit.core import manager as b_manager
  16. from bandit.core import utils
  17. BASE_CONFIG = "bandit.yaml"
  18. LOG = logging.getLogger()
  19. def _init_logger(log_level=logging.INFO, log_format=None):
  20. """Initialize the logger.
  21. :param debug: Whether to enable debug mode
  22. :return: An instantiated logging instance
  23. """
  24. LOG.handlers = []
  25. if not log_format:
  26. # default log format
  27. log_format_string = constants.log_format_string
  28. else:
  29. log_format_string = log_format
  30. logging.captureWarnings(True)
  31. LOG.setLevel(log_level)
  32. handler = logging.StreamHandler(sys.stderr)
  33. handler.setFormatter(logging.Formatter(log_format_string))
  34. LOG.addHandler(handler)
  35. LOG.debug("logging initialized")
  36. def _get_options_from_ini(ini_path, target):
  37. """Return a dictionary of config options or None if we can't load any."""
  38. ini_file = None
  39. if ini_path:
  40. ini_file = ini_path
  41. else:
  42. bandit_files = []
  43. for t in target:
  44. for root, _, filenames in os.walk(t):
  45. for filename in fnmatch.filter(filenames, ".bandit"):
  46. bandit_files.append(os.path.join(root, filename))
  47. if len(bandit_files) > 1:
  48. LOG.error(
  49. "Multiple .bandit files found - scan separately or "
  50. "choose one with --ini\n\t%s",
  51. ", ".join(bandit_files),
  52. )
  53. sys.exit(2)
  54. elif len(bandit_files) == 1:
  55. ini_file = bandit_files[0]
  56. LOG.info("Found project level .bandit file: %s", bandit_files[0])
  57. if ini_file:
  58. return utils.parse_ini_file(ini_file)
  59. else:
  60. return None
  61. def _init_extensions():
  62. from bandit.core import extension_loader as ext_loader
  63. return ext_loader.MANAGER
  64. def _log_option_source(default_val, arg_val, ini_val, option_name):
  65. """It's useful to show the source of each option."""
  66. # When default value is not defined, arg_val and ini_val is deterministic
  67. if default_val is None:
  68. if arg_val:
  69. LOG.info("Using command line arg for %s", option_name)
  70. return arg_val
  71. elif ini_val:
  72. LOG.info("Using ini file for %s", option_name)
  73. return ini_val
  74. else:
  75. return None
  76. # No value passed to commad line and default value is used
  77. elif default_val == arg_val:
  78. return ini_val if ini_val else arg_val
  79. # Certainly a value is passed to commad line
  80. else:
  81. return arg_val
  82. def _running_under_virtualenv():
  83. if hasattr(sys, "real_prefix"):
  84. return True
  85. elif sys.prefix != getattr(sys, "base_prefix", sys.prefix):
  86. return True
  87. def _get_profile(config, profile_name, config_path):
  88. profile = {}
  89. if profile_name:
  90. profiles = config.get_option("profiles") or {}
  91. profile = profiles.get(profile_name)
  92. if profile is None:
  93. raise utils.ProfileNotFound(config_path, profile_name)
  94. LOG.debug("read in legacy profile '%s': %s", profile_name, profile)
  95. else:
  96. profile["include"] = set(config.get_option("tests") or [])
  97. profile["exclude"] = set(config.get_option("skips") or [])
  98. return profile
  99. def _log_info(args, profile):
  100. inc = ",".join([t for t in profile["include"]]) or "None"
  101. exc = ",".join([t for t in profile["exclude"]]) or "None"
  102. LOG.info("profile include tests: %s", inc)
  103. LOG.info("profile exclude tests: %s", exc)
  104. LOG.info("cli include tests: %s", args.tests)
  105. LOG.info("cli exclude tests: %s", args.skips)
  106. def main():
  107. """Bandit CLI."""
  108. # bring our logging stuff up as early as possible
  109. debug = (
  110. logging.DEBUG
  111. if "-d" in sys.argv or "--debug" in sys.argv
  112. else logging.INFO
  113. )
  114. _init_logger(debug)
  115. extension_mgr = _init_extensions()
  116. baseline_formatters = [
  117. f.name
  118. for f in filter(
  119. lambda x: hasattr(x.plugin, "_accepts_baseline"),
  120. extension_mgr.formatters,
  121. )
  122. ]
  123. # now do normal startup
  124. parser = argparse.ArgumentParser(
  125. description="Bandit - a Python source code security analyzer",
  126. formatter_class=argparse.RawDescriptionHelpFormatter,
  127. )
  128. parser.add_argument(
  129. "targets",
  130. metavar="targets",
  131. type=str,
  132. nargs="*",
  133. help="source file(s) or directory(s) to be tested",
  134. )
  135. parser.add_argument(
  136. "-r",
  137. "--recursive",
  138. dest="recursive",
  139. action="store_true",
  140. help="find and process files in subdirectories",
  141. )
  142. parser.add_argument(
  143. "-a",
  144. "--aggregate",
  145. dest="agg_type",
  146. action="store",
  147. default="file",
  148. type=str,
  149. choices=["file", "vuln"],
  150. help="aggregate output by vulnerability (default) or by filename",
  151. )
  152. parser.add_argument(
  153. "-n",
  154. "--number",
  155. dest="context_lines",
  156. action="store",
  157. default=3,
  158. type=int,
  159. help="maximum number of code lines to output for each issue",
  160. )
  161. parser.add_argument(
  162. "-c",
  163. "--configfile",
  164. dest="config_file",
  165. action="store",
  166. default=None,
  167. type=str,
  168. help="optional config file to use for selecting plugins and "
  169. "overriding defaults",
  170. )
  171. parser.add_argument(
  172. "-p",
  173. "--profile",
  174. dest="profile",
  175. action="store",
  176. default=None,
  177. type=str,
  178. help="profile to use (defaults to executing all tests)",
  179. )
  180. parser.add_argument(
  181. "-t",
  182. "--tests",
  183. dest="tests",
  184. action="store",
  185. default=None,
  186. type=str,
  187. help="comma-separated list of test IDs to run",
  188. )
  189. parser.add_argument(
  190. "-s",
  191. "--skip",
  192. dest="skips",
  193. action="store",
  194. default=None,
  195. type=str,
  196. help="comma-separated list of test IDs to skip",
  197. )
  198. severity_group = parser.add_mutually_exclusive_group(required=False)
  199. severity_group.add_argument(
  200. "-l",
  201. "--level",
  202. dest="severity",
  203. action="count",
  204. default=1,
  205. help="report only issues of a given severity level or "
  206. "higher (-l for LOW, -ll for MEDIUM, -lll for HIGH)",
  207. )
  208. severity_group.add_argument(
  209. "--severity-level",
  210. dest="severity_string",
  211. action="store",
  212. help="report only issues of a given severity level or higher."
  213. ' "all" and "low" are likely to produce the same results, but it'
  214. " is possible for rules to be undefined which will"
  215. ' not be listed in "low".',
  216. choices=["all", "low", "medium", "high"],
  217. )
  218. confidence_group = parser.add_mutually_exclusive_group(required=False)
  219. confidence_group.add_argument(
  220. "-i",
  221. "--confidence",
  222. dest="confidence",
  223. action="count",
  224. default=1,
  225. help="report only issues of a given confidence level or "
  226. "higher (-i for LOW, -ii for MEDIUM, -iii for HIGH)",
  227. )
  228. confidence_group.add_argument(
  229. "--confidence-level",
  230. dest="confidence_string",
  231. action="store",
  232. help="report only issues of a given confidence level or higher."
  233. ' "all" and "low" are likely to produce the same results, but it'
  234. " is possible for rules to be undefined which will"
  235. ' not be listed in "low".',
  236. choices=["all", "low", "medium", "high"],
  237. )
  238. output_format = (
  239. "screen"
  240. if (
  241. sys.stdout.isatty()
  242. and os.getenv("NO_COLOR") is None
  243. and os.getenv("TERM") != "dumb"
  244. )
  245. else "txt"
  246. )
  247. parser.add_argument(
  248. "-f",
  249. "--format",
  250. dest="output_format",
  251. action="store",
  252. default=output_format,
  253. help="specify output format",
  254. choices=sorted(extension_mgr.formatter_names),
  255. )
  256. parser.add_argument(
  257. "--msg-template",
  258. action="store",
  259. default=None,
  260. help="specify output message template"
  261. " (only usable with --format custom),"
  262. " see CUSTOM FORMAT section"
  263. " for list of available values",
  264. )
  265. parser.add_argument(
  266. "-o",
  267. "--output",
  268. dest="output_file",
  269. action="store",
  270. nargs="?",
  271. type=argparse.FileType("w", encoding="utf-8"),
  272. default=sys.stdout,
  273. help="write report to filename",
  274. )
  275. group = parser.add_mutually_exclusive_group(required=False)
  276. group.add_argument(
  277. "-v",
  278. "--verbose",
  279. dest="verbose",
  280. action="store_true",
  281. help="output extra information like excluded and included files",
  282. )
  283. parser.add_argument(
  284. "-d",
  285. "--debug",
  286. dest="debug",
  287. action="store_true",
  288. help="turn on debug mode",
  289. )
  290. group.add_argument(
  291. "-q",
  292. "--quiet",
  293. "--silent",
  294. dest="quiet",
  295. action="store_true",
  296. help="only show output in the case of an error",
  297. )
  298. parser.add_argument(
  299. "--ignore-nosec",
  300. dest="ignore_nosec",
  301. action="store_true",
  302. help="do not skip lines with # nosec comments",
  303. )
  304. parser.add_argument(
  305. "-x",
  306. "--exclude",
  307. dest="excluded_paths",
  308. action="store",
  309. default=",".join(constants.EXCLUDE),
  310. help="comma-separated list of paths (glob patterns "
  311. "supported) to exclude from scan "
  312. "(note that these are in addition to the excluded "
  313. "paths provided in the config file) (default: "
  314. + ",".join(constants.EXCLUDE)
  315. + ")",
  316. )
  317. parser.add_argument(
  318. "-b",
  319. "--baseline",
  320. dest="baseline",
  321. action="store",
  322. default=None,
  323. help="path of a baseline report to compare against "
  324. "(only JSON-formatted files are accepted)",
  325. )
  326. parser.add_argument(
  327. "--ini",
  328. dest="ini_path",
  329. action="store",
  330. default=None,
  331. help="path to a .bandit file that supplies command line arguments",
  332. )
  333. parser.add_argument(
  334. "--exit-zero",
  335. action="store_true",
  336. dest="exit_zero",
  337. default=False,
  338. help="exit with 0, " "even with results found",
  339. )
  340. python_ver = sys.version.replace("\n", "")
  341. parser.add_argument(
  342. "--version",
  343. action="version",
  344. version="%(prog)s {version}\n python version = {python}".format(
  345. version=bandit.__version__, python=python_ver
  346. ),
  347. )
  348. parser.set_defaults(debug=False)
  349. parser.set_defaults(verbose=False)
  350. parser.set_defaults(quiet=False)
  351. parser.set_defaults(ignore_nosec=False)
  352. plugin_info = [
  353. f"{a[0]}\t{a[1].name}" for a in extension_mgr.plugins_by_id.items()
  354. ]
  355. blacklist_info = []
  356. for a in extension_mgr.blacklist.items():
  357. for b in a[1]:
  358. blacklist_info.append("{}\t{}".format(b["id"], b["name"]))
  359. plugin_list = "\n\t".join(sorted(set(plugin_info + blacklist_info)))
  360. dedent_text = textwrap.dedent(
  361. """
  362. CUSTOM FORMATTING
  363. -----------------
  364. Available tags:
  365. {abspath}, {relpath}, {line}, {col}, {test_id},
  366. {severity}, {msg}, {confidence}, {range}
  367. Example usage:
  368. Default template:
  369. bandit -r examples/ --format custom --msg-template \\
  370. "{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}"
  371. Provides same output as:
  372. bandit -r examples/ --format custom
  373. Tags can also be formatted in python string.format() style:
  374. bandit -r examples/ --format custom --msg-template \\
  375. "{relpath:20.20s}: {line:03}: {test_id:^8}: DEFECT: {msg:>20}"
  376. See python documentation for more information about formatting style:
  377. https://docs.python.org/3/library/string.html
  378. The following tests were discovered and loaded:
  379. -----------------------------------------------
  380. """
  381. )
  382. parser.epilog = dedent_text + f"\t{plugin_list}"
  383. # setup work - parse arguments, and initialize BanditManager
  384. args = parser.parse_args()
  385. # Check if `--msg-template` is not present without custom formatter
  386. if args.output_format != "custom" and args.msg_template is not None:
  387. parser.error("--msg-template can only be used with --format=custom")
  388. # Check if confidence or severity level have been specified with strings
  389. if args.severity_string is not None:
  390. if args.severity_string == "all":
  391. args.severity = 1
  392. elif args.severity_string == "low":
  393. args.severity = 2
  394. elif args.severity_string == "medium":
  395. args.severity = 3
  396. elif args.severity_string == "high":
  397. args.severity = 4
  398. # Other strings will be blocked by argparse
  399. if args.confidence_string is not None:
  400. if args.confidence_string == "all":
  401. args.confidence = 1
  402. elif args.confidence_string == "low":
  403. args.confidence = 2
  404. elif args.confidence_string == "medium":
  405. args.confidence = 3
  406. elif args.confidence_string == "high":
  407. args.confidence = 4
  408. # Other strings will be blocked by argparse
  409. try:
  410. b_conf = b_config.BanditConfig(config_file=args.config_file)
  411. except utils.ConfigError as e:
  412. LOG.error(e)
  413. sys.exit(2)
  414. # Handle .bandit files in projects to pass cmdline args from file
  415. ini_options = _get_options_from_ini(args.ini_path, args.targets)
  416. if ini_options:
  417. # prefer command line, then ini file
  418. args.excluded_paths = _log_option_source(
  419. parser.get_default("excluded_paths"),
  420. args.excluded_paths,
  421. ini_options.get("exclude"),
  422. "excluded paths",
  423. )
  424. args.skips = _log_option_source(
  425. parser.get_default("skips"),
  426. args.skips,
  427. ini_options.get("skips"),
  428. "skipped tests",
  429. )
  430. args.tests = _log_option_source(
  431. parser.get_default("tests"),
  432. args.tests,
  433. ini_options.get("tests"),
  434. "selected tests",
  435. )
  436. ini_targets = ini_options.get("targets")
  437. if ini_targets:
  438. ini_targets = ini_targets.split(",")
  439. args.targets = _log_option_source(
  440. parser.get_default("targets"),
  441. args.targets,
  442. ini_targets,
  443. "selected targets",
  444. )
  445. # TODO(tmcpeak): any other useful options to pass from .bandit?
  446. args.recursive = _log_option_source(
  447. parser.get_default("recursive"),
  448. args.recursive,
  449. ini_options.get("recursive"),
  450. "recursive scan",
  451. )
  452. args.agg_type = _log_option_source(
  453. parser.get_default("agg_type"),
  454. args.agg_type,
  455. ini_options.get("aggregate"),
  456. "aggregate output type",
  457. )
  458. args.context_lines = _log_option_source(
  459. parser.get_default("context_lines"),
  460. args.context_lines,
  461. int(ini_options.get("number") or 0) or None,
  462. "max code lines output for issue",
  463. )
  464. args.profile = _log_option_source(
  465. parser.get_default("profile"),
  466. args.profile,
  467. ini_options.get("profile"),
  468. "profile",
  469. )
  470. args.severity = _log_option_source(
  471. parser.get_default("severity"),
  472. args.severity,
  473. ini_options.get("level"),
  474. "severity level",
  475. )
  476. args.confidence = _log_option_source(
  477. parser.get_default("confidence"),
  478. args.confidence,
  479. ini_options.get("confidence"),
  480. "confidence level",
  481. )
  482. args.output_format = _log_option_source(
  483. parser.get_default("output_format"),
  484. args.output_format,
  485. ini_options.get("format"),
  486. "output format",
  487. )
  488. args.msg_template = _log_option_source(
  489. parser.get_default("msg_template"),
  490. args.msg_template,
  491. ini_options.get("msg-template"),
  492. "output message template",
  493. )
  494. args.output_file = _log_option_source(
  495. parser.get_default("output_file"),
  496. args.output_file,
  497. ini_options.get("output"),
  498. "output file",
  499. )
  500. args.verbose = _log_option_source(
  501. parser.get_default("verbose"),
  502. args.verbose,
  503. ini_options.get("verbose"),
  504. "output extra information",
  505. )
  506. args.debug = _log_option_source(
  507. parser.get_default("debug"),
  508. args.debug,
  509. ini_options.get("debug"),
  510. "debug mode",
  511. )
  512. args.quiet = _log_option_source(
  513. parser.get_default("quiet"),
  514. args.quiet,
  515. ini_options.get("quiet"),
  516. "silent mode",
  517. )
  518. args.ignore_nosec = _log_option_source(
  519. parser.get_default("ignore_nosec"),
  520. args.ignore_nosec,
  521. ini_options.get("ignore-nosec"),
  522. "do not skip lines with # nosec",
  523. )
  524. args.baseline = _log_option_source(
  525. parser.get_default("baseline"),
  526. args.baseline,
  527. ini_options.get("baseline"),
  528. "path of a baseline report",
  529. )
  530. if not args.targets:
  531. parser.print_usage()
  532. sys.exit(2)
  533. # if the log format string was set in the options, reinitialize
  534. if b_conf.get_option("log_format"):
  535. log_format = b_conf.get_option("log_format")
  536. _init_logger(log_level=logging.DEBUG, log_format=log_format)
  537. if args.quiet:
  538. _init_logger(log_level=logging.WARN)
  539. try:
  540. profile = _get_profile(b_conf, args.profile, args.config_file)
  541. _log_info(args, profile)
  542. profile["include"].update(args.tests.split(",") if args.tests else [])
  543. profile["exclude"].update(args.skips.split(",") if args.skips else [])
  544. extension_mgr.validate_profile(profile)
  545. except (utils.ProfileNotFound, ValueError) as e:
  546. LOG.error(e)
  547. sys.exit(2)
  548. b_mgr = b_manager.BanditManager(
  549. b_conf,
  550. args.agg_type,
  551. args.debug,
  552. profile=profile,
  553. verbose=args.verbose,
  554. quiet=args.quiet,
  555. ignore_nosec=args.ignore_nosec,
  556. )
  557. if args.baseline is not None:
  558. try:
  559. with open(args.baseline) as bl:
  560. data = bl.read()
  561. b_mgr.populate_baseline(data)
  562. except OSError:
  563. LOG.warning("Could not open baseline report: %s", args.baseline)
  564. sys.exit(2)
  565. if args.output_format not in baseline_formatters:
  566. LOG.warning(
  567. "Baseline must be used with one of the following "
  568. "formats: " + str(baseline_formatters)
  569. )
  570. sys.exit(2)
  571. if args.output_format != "json":
  572. if args.config_file:
  573. LOG.info("using config: %s", args.config_file)
  574. LOG.info(
  575. "running on Python %d.%d.%d",
  576. sys.version_info.major,
  577. sys.version_info.minor,
  578. sys.version_info.micro,
  579. )
  580. # initiate file discovery step within Bandit Manager
  581. b_mgr.discover_files(args.targets, args.recursive, args.excluded_paths)
  582. if not b_mgr.b_ts.tests:
  583. LOG.error("No tests would be run, please check the profile.")
  584. sys.exit(2)
  585. # initiate execution of tests within Bandit Manager
  586. b_mgr.run_tests()
  587. LOG.debug(b_mgr.b_ma)
  588. LOG.debug(b_mgr.metrics)
  589. # trigger output of results by Bandit Manager
  590. sev_level = constants.RANKING[args.severity - 1]
  591. conf_level = constants.RANKING[args.confidence - 1]
  592. b_mgr.output_results(
  593. args.context_lines,
  594. sev_level,
  595. conf_level,
  596. args.output_file,
  597. args.output_format,
  598. args.msg_template,
  599. )
  600. if (
  601. b_mgr.results_count(sev_filter=sev_level, conf_filter=conf_level) > 0
  602. and not args.exit_zero
  603. ):
  604. sys.exit(1)
  605. else:
  606. sys.exit(0)
  607. if __name__ == "__main__":
  608. main()