printerxpl-forge 6.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. nse/README.md +204 -0
  2. nse/__init__.py +6 -0
  3. nse/install_nse.py +412 -0
  4. nse/lib/printerxpl.lua +238 -0
  5. nse/scripts/cups-info.nse +74 -0
  6. nse/scripts/cups-queue-info.nse +43 -0
  7. nse/scripts/hp-printers-cve-2022-1026.nse +121 -0
  8. nse/scripts/http-device-mac.nse +107 -0
  9. nse/scripts/http-hp-ilo-info.nse +121 -0
  10. nse/scripts/http-info-xerox-enum.nse +101 -0
  11. nse/scripts/http-vuln-cve2022-1026.nse +158 -0
  12. nse/scripts/lexmark-config.nse +89 -0
  13. nse/scripts/pjl-ready-message.nse +106 -0
  14. nse/scripts/printer-banner.nse +217 -0
  15. nse/scripts/printer-cups-rce.nse +189 -0
  16. nse/scripts/printer-cve-detect.nse +279 -0
  17. nse/scripts/printer-discover.nse +205 -0
  18. nse/scripts/printer-firmware-exposed.nse +219 -0
  19. nse/scripts/printer-hp-pjl.nse +192 -0
  20. nse/scripts/printer-http-ews.nse +293 -0
  21. nse/scripts/printer-ipp-info.nse +235 -0
  22. nse/scripts/printer-lexmark-ipp.nse +203 -0
  23. nse/scripts/printer-passback.nse +204 -0
  24. nse/scripts/printer-pjl-info.nse +146 -0
  25. nse/scripts/printer-printnightmare.nse +211 -0
  26. nse/scripts/printer-snmp-info.nse +176 -0
  27. nse/scripts/printer-vuln-check.nse +256 -0
  28. nse/scripts/snmp-device-mac.nse +93 -0
  29. nse/scripts/snmp-info.nse +146 -0
  30. nse/scripts/snmp-sysdescr.nse +70 -0
  31. printerxpl_forge-6.2.0.dist-info/METADATA +919 -0
  32. printerxpl_forge-6.2.0.dist-info/RECORD +97 -0
  33. printerxpl_forge-6.2.0.dist-info/WHEEL +5 -0
  34. printerxpl_forge-6.2.0.dist-info/entry_points.txt +4 -0
  35. printerxpl_forge-6.2.0.dist-info/licenses/LICENSE +21 -0
  36. printerxpl_forge-6.2.0.dist-info/top_level.txt +4 -0
  37. src/assets/fonts/gunplay.pfa +1671 -0
  38. src/assets/fonts/kshandwrt.pfa +315 -0
  39. src/assets/fonts/laksoner.pfa +2402 -0
  40. src/assets/fonts/paintcans.pfa +9699 -0
  41. src/assets/fonts/stencilod.pfa +4076 -0
  42. src/assets/fonts/takecover.pfa +26138 -0
  43. src/assets/fonts/topsecret.pfa +6652 -0
  44. src/assets/fonts/whoa.pfa +773 -0
  45. src/assets/mibs/HOST-RESOURCES-MIB +1540 -0
  46. src/assets/mibs/Printer-MIB +4389 -0
  47. src/assets/mibs/README.md +9 -0
  48. src/assets/mibs/SNMPv2-MIB +854 -0
  49. src/assets/overlays/hacker.eps +596 -0
  50. src/assets/overlays/smiley.eps +214 -0
  51. src/assets/overlays/smiley2.eps +240 -0
  52. src/core/attack_orchestrator.py +1025 -0
  53. src/core/capabilities.py +323 -0
  54. src/core/destructive_audit.py +430 -0
  55. src/core/discovery.py +488 -0
  56. src/core/osdetect.py +74 -0
  57. src/core/poly_runner.py +579 -0
  58. src/core/printer.py +1426 -0
  59. src/main.py +2134 -0
  60. src/modules/install_printer.py +318 -0
  61. src/modules/login_bruteforce.py +852 -0
  62. src/modules/pcl.py +506 -0
  63. src/modules/pjl.py +3575 -0
  64. src/modules/print_job.py +1290 -0
  65. src/modules/ps.py +1102 -0
  66. src/payloads/__init__.py +98 -0
  67. src/payloads/assets/overlays/notice.eps +9 -0
  68. src/protocols/__init__.py +19 -0
  69. src/protocols/firmware.py +738 -0
  70. src/protocols/ipp.py +216 -0
  71. src/protocols/ipp_attacks.py +609 -0
  72. src/protocols/lpd.py +141 -0
  73. src/protocols/network_map.py +1004 -0
  74. src/protocols/raw.py +173 -0
  75. src/protocols/smb.py +359 -0
  76. src/protocols/ssrf_pivot.py +427 -0
  77. src/protocols/storage.py +587 -0
  78. src/ui/__init__.py +6 -0
  79. src/ui/interactive.py +742 -0
  80. src/ui/spinner.py +112 -0
  81. src/ui/tables.py +132 -0
  82. src/utils/banner_grabber.py +852 -0
  83. src/utils/codebook.py +456 -0
  84. src/utils/config.py +522 -0
  85. src/utils/cve_loader.py +158 -0
  86. src/utils/default_creds.py +134 -0
  87. src/utils/discovery_online.py +1327 -0
  88. src/utils/exploit_manager.py +805 -0
  89. src/utils/fuzzer.py +220 -0
  90. src/utils/helper.py +732 -0
  91. src/utils/local_printers.py +307 -0
  92. src/utils/ml_engine.py +491 -0
  93. src/utils/operators.py +474 -0
  94. src/utils/ports.py +234 -0
  95. src/utils/vuln_scanner.py +823 -0
  96. src/utils/wordlist_loader.py +412 -0
  97. src/version.py +36 -0
@@ -0,0 +1,430 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ PrinterXPL-Forge — Destructive Attack Audit Engine
5
+ ===================================================
6
+ Scans a target printer for all known IRREVERSIBLE / PHYSICAL-DAMAGE
7
+ vulnerabilities and produces a structured report.
8
+
9
+ Usage (CLI):
10
+ python src/main.py <target> --destructive-audit
11
+ python src/main.py <target> --destructive-audit --no-dry # LIVE execution
12
+
13
+ Modules covered:
14
+ 1. research-pjl-nvram-damage NVRAM wear via PJL DEFAULT (physical)
15
+ 2. research-brother-nvram Brother NVRAM exhaustion (physical)
16
+ 3. research-generic-pjl-nvram Generic NVRAM write access (physical risk)
17
+ 4. research-snmp-factory-reset SNMP unauthenticated factory reset
18
+ 5. research-xerox-pjl-dlm Xerox DLM firmware brick
19
+ 6. research-xerox-firmware-root Xerox firmware rootkit injection
20
+ 7. research-fuser-thermal-attack Fuser thermal runaway (fire/hardware)
21
+ 8. research-motor-jam-attack Motor/mechanical destruction
22
+ 9. research-laser-scanner-attack Laser diode/drum damage
23
+ 10. edb-45273 HP persistent root via PJL path traversal
24
+
25
+ ==========================================================================
26
+ WARNING: All live modes (dry_run=False / --no-dry) cause IRREVERSIBLE DAMAGE.
27
+ Use exclusively in authorized penetration testing lab environments.
28
+ Operators bear full legal and physical safety responsibility.
29
+ ==========================================================================
30
+
31
+ Author: Andre Henrique (@mrhenrike) | Uniao Geek — https://github.com/Uniao-Geek
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import importlib.util
37
+ import socket
38
+ import sys
39
+ import time
40
+ from pathlib import Path
41
+ from typing import Dict, List, Optional, Tuple
42
+
43
+ # ── ANSI ──────────────────────────────────────────────────────────────────────
44
+ _RST = '\033[0m'
45
+ _BLD = '\033[1m'
46
+ _RED = '\033[1;31m'
47
+ _YEL = '\033[1;33m'
48
+ _GRN = '\033[0;32m'
49
+ _CYN = '\033[1;36m'
50
+ _DIM = '\033[2;37m'
51
+ _MGT = '\033[1;35m'
52
+ _WHT = '\033[1;37m'
53
+
54
+ # ── Root path resolution ───────────────────────────────────────────────────────
55
+ _SRC_DIR = Path(__file__).resolve().parent.parent # src/
56
+ _ROOT_DIR = _SRC_DIR.parent # project root
57
+ _XPL_DIR = _ROOT_DIR / 'xpl'
58
+
59
+ # ── Destructive module registry ────────────────────────────────────────────────
60
+ # Each entry: (module_id, xpl_subpath, port, label, damage_class)
61
+ DESTRUCTIVE_MODULES: List[Tuple[str, str, int, str, str]] = [
62
+ (
63
+ "research-fuser-thermal-attack",
64
+ "research/research-fuser-thermal-attack/exploit.py",
65
+ 9100,
66
+ "Fuser Thermal Runaway (fire / hardware melt)",
67
+ "PHYSICAL",
68
+ ),
69
+ (
70
+ "research-motor-jam-attack",
71
+ "research/research-motor-jam-attack/exploit.py",
72
+ 9100,
73
+ "Motor Jamming / Gear Strip (mechanical destruction)",
74
+ "PHYSICAL",
75
+ ),
76
+ (
77
+ "research-laser-scanner-attack",
78
+ "research/research-laser-scanner-attack/exploit.py",
79
+ 9100,
80
+ "Laser Scanner Damage (drum / diode destruction)",
81
+ "PHYSICAL",
82
+ ),
83
+ (
84
+ "research-pjl-nvram-damage",
85
+ "research/research-pjl-nvram-damage/exploit.py",
86
+ 9100,
87
+ "NVRAM Write Exhaustion (chip burnout — multi-vendor)",
88
+ "NVRAM",
89
+ ),
90
+ (
91
+ "research-brother-nvram",
92
+ "research/research-brother-nvram/exploit.py",
93
+ 9100,
94
+ "Brother NVRAM Exhaustion (200k write cycles — permanent)",
95
+ "NVRAM",
96
+ ),
97
+ (
98
+ "research-generic-pjl-nvram",
99
+ "research/research-generic-pjl-nvram/exploit.py",
100
+ 9100,
101
+ "Generic PJL NVRAM R/W (NVRAM access — physical risk)",
102
+ "NVRAM",
103
+ ),
104
+ (
105
+ "research-snmp-factory-reset",
106
+ "research/research-snmp-factory-reset/exploit.py",
107
+ 161,
108
+ "SNMP Unauthenticated Reset (factory wipe — multi-vendor)",
109
+ "CONFIG",
110
+ ),
111
+ (
112
+ "research-xerox-pjl-dlm",
113
+ "research/research-xerox-pjl-dlm/exploit.py",
114
+ 9100,
115
+ "Xerox DLM Firmware Brick (PJL firmware injection)",
116
+ "FIRMWARE",
117
+ ),
118
+ (
119
+ "research-xerox-firmware-root",
120
+ "research/research-xerox-firmware-root/exploit.py",
121
+ 80,
122
+ "Xerox Firmware Rootkit (HTTP firmware upload brick)",
123
+ "FIRMWARE",
124
+ ),
125
+ (
126
+ "edb-45273",
127
+ "edb-45273/exploit.py",
128
+ 9100,
129
+ "HP PJL → Persistent Root (CVE-2017-2741 — boot backdoor)",
130
+ "FIRMWARE",
131
+ ),
132
+ ]
133
+
134
+ _DAMAGE_CLR = {
135
+ "PHYSICAL": _RED,
136
+ "NVRAM": _MGT,
137
+ "FIRMWARE": _YEL,
138
+ "CONFIG": _CYN,
139
+ }
140
+
141
+ _DAMAGE_LABEL = {
142
+ "PHYSICAL": "PHYSICAL DESTRUCTION",
143
+ "NVRAM": "NVRAM WEAR / BRICK",
144
+ "FIRMWARE": "FIRMWARE BRICK / ROOT",
145
+ "CONFIG": "CONFIG WIPE",
146
+ }
147
+
148
+
149
+ # ── Module loader ──────────────────────────────────────────────────────────────
150
+ def _load_exploit(rel_path: str):
151
+ """Dynamically load an exploit module from xpl/."""
152
+ full = _XPL_DIR / rel_path
153
+ if not full.exists():
154
+ return None
155
+ spec = importlib.util.spec_from_file_location(full.stem, str(full))
156
+ if spec is None or spec.loader is None:
157
+ return None
158
+ mod = importlib.util.module_from_spec(spec)
159
+ try:
160
+ spec.loader.exec_module(mod)
161
+ except Exception:
162
+ return None
163
+ return mod
164
+
165
+
166
+ # ── Port connectivity check ────────────────────────────────────────────────────
167
+ def _port_open(host: str, port: int, timeout: float = 4.0) -> bool:
168
+ try:
169
+ proto = socket.IPPROTO_UDP if port == 161 else socket.IPPROTO_TCP
170
+ if port == 161:
171
+ # SNMP: send v2c GetRequest with community 'public'
172
+ pkt = bytes([
173
+ 0x30, 0x26, 0x02, 0x01, 0x01, 0x04, 0x06,
174
+ 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63,
175
+ 0xa0, 0x19, 0x02, 0x04, 0x01, 0x02, 0x03, 0x04,
176
+ 0x02, 0x01, 0x00, 0x02, 0x01, 0x00, 0x30, 0x0b,
177
+ 0x30, 0x09, 0x06, 0x05, 0x2b, 0x06, 0x01, 0x02, 0x01,
178
+ 0x05, 0x00,
179
+ ])
180
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
181
+ s.settimeout(timeout)
182
+ s.sendto(pkt, (host, port))
183
+ try:
184
+ s.recv(256)
185
+ return True
186
+ except socket.timeout:
187
+ return True # Sent = port is reachable (SNMP may not respond)
188
+ else:
189
+ with socket.create_connection((host, port), timeout=timeout):
190
+ return True
191
+ except Exception:
192
+ return False
193
+
194
+
195
+ # ── Main audit function ─────────────────────────────────────────────────────────
196
+ def run_destructive_audit(
197
+ host: str,
198
+ port_overrides: Optional[Dict[str, int]] = None,
199
+ dry_run: bool = True,
200
+ selected_ids: Optional[List[str]] = None,
201
+ timeout: float = 10.0,
202
+ verbose: bool = False,
203
+ ) -> Dict:
204
+ """
205
+ Scan a target for all destructive/irreversible vulnerabilities.
206
+
207
+ Args:
208
+ host : Target IP or hostname.
209
+ port_overrides : Dict of {port_key: port_number} for custom ports.
210
+ Keys: 'raw' (9100), 'snmp' (161), 'http' (80).
211
+ dry_run : True = check/assess only, never send destructive payload.
212
+ False = LIVE execution (IRREVERSIBLE DAMAGE).
213
+ selected_ids : List of module IDs to run (None = all modules).
214
+ timeout : Per-module connection timeout.
215
+ verbose : Print detailed evidence for each module.
216
+
217
+ Returns:
218
+ dict with:
219
+ 'target' : host
220
+ 'dry_run' : bool
221
+ 'total' : total modules checked
222
+ 'vulnerable' : list of vulnerable module IDs
223
+ 'not_vuln' : list of not-vulnerable module IDs
224
+ 'errors' : list of (module_id, error_string)
225
+ 'results' : dict of {module_id: result_dict}
226
+ 'report' : formatted text report
227
+ """
228
+ overrides = port_overrides or {}
229
+ port_map = {'raw': 9100, 'snmp': 161, 'http': 80}
230
+ for k, v in overrides.items():
231
+ port_map[k] = v
232
+
233
+ audit: Dict = {
234
+ 'target': host,
235
+ 'dry_run': dry_run,
236
+ 'total': 0,
237
+ 'vulnerable': [],
238
+ 'not_vuln': [],
239
+ 'errors': [],
240
+ 'results': {},
241
+ 'report': '',
242
+ }
243
+
244
+ lines: List[str] = []
245
+
246
+ def _hdr(text: str, char: str = '═') -> str:
247
+ return f"\n {_CYN}{char * 60}{_RST}\n {_BLD}{_CYN}{text}{_RST}"
248
+
249
+ def _sep(char: str = '─') -> str:
250
+ return f" {_DIM}{char * 60}{_RST}"
251
+
252
+ # ── Header ────────────────────────────────────────────────────────────────
253
+ mode_label = (
254
+ f"{_RED}[LIVE — DESTRUCTIVE]{_RST}" if not dry_run
255
+ else f"{_GRN}[DRY-RUN / ASSESS]{_RST}"
256
+ )
257
+ lines += [
258
+ _hdr("PrinterXPL-Forge — Destructive Attack Audit"),
259
+ f"\n Target : {_WHT}{host}{_RST}",
260
+ f" Mode : {mode_label}",
261
+ f" Modules: {len(DESTRUCTIVE_MODULES)} irreversible attack vectors",
262
+ _sep(),
263
+ ]
264
+
265
+ if not dry_run:
266
+ lines += [
267
+ f"\n {_RED}{_BLD}!!! LIVE MODE ACTIVE !!!{_RST}",
268
+ f" {_RED}Destructive payloads WILL be sent. Hardware damage is IRREVERSIBLE.{_RST}",
269
+ f" {_RED}Ensure written authorization and fire/safety controls are in place.{_RST}",
270
+ _sep(),
271
+ ]
272
+
273
+ # ── Module loop ───────────────────────────────────────────────────────────
274
+ for mod_id, rel_path, default_port, label, damage_class in DESTRUCTIVE_MODULES:
275
+
276
+ # Filter by selected IDs if specified
277
+ if selected_ids and mod_id not in selected_ids:
278
+ continue
279
+
280
+ audit['total'] += 1
281
+
282
+ # Resolve port
283
+ port_key = 'snmp' if default_port == 161 else ('http' if default_port == 80 else 'raw')
284
+ port = port_map.get(port_key, default_port)
285
+
286
+ dc = _DAMAGE_CLR.get(damage_class, _DIM)
287
+ dl = _DAMAGE_LABEL.get(damage_class, damage_class)
288
+ lines.append(f"\n {_YEL}[{audit['total']:02d}]{_RST} {label}")
289
+ lines.append(f" {dc}{dl}{_RST} │ port {port} │ {_DIM}{rel_path}{_RST}")
290
+
291
+ # 1. Port connectivity check
292
+ if not _port_open(host, port, timeout=4.0):
293
+ lines.append(f" {_DIM}Port {port} closed — SKIP{_RST}")
294
+ audit['not_vuln'].append(mod_id)
295
+ audit['results'][mod_id] = {'status': 'port_closed', 'port': port}
296
+ continue
297
+
298
+ lines.append(f" {_GRN}Port {port} open{_RST} — loading module...")
299
+
300
+ # 2. Load exploit module
301
+ mod = _load_exploit(rel_path)
302
+ if mod is None:
303
+ err = f"Could not load exploit module: {rel_path}"
304
+ lines.append(f" {_RED}ERROR: {err}{_RST}")
305
+ audit['errors'].append((mod_id, err))
306
+ continue
307
+
308
+ # 3. Run check()
309
+ try:
310
+ check_fn = getattr(mod, 'check', None)
311
+ is_vuln = check_fn(host, port, timeout) if check_fn else True
312
+ except Exception as exc:
313
+ is_vuln = False
314
+ audit['errors'].append((mod_id, f"check() error: {exc}"))
315
+
316
+ vuln_label = (
317
+ f"{_RED}VULNERABLE{_RST}" if is_vuln
318
+ else f"{_DIM}not vulnerable{_RST}"
319
+ )
320
+ lines.append(f" Vulnerability check: {vuln_label}")
321
+
322
+ result_entry: Dict = {
323
+ 'status': 'vulnerable' if is_vuln else 'not_vulnerable',
324
+ 'module_id': mod_id,
325
+ 'port': port,
326
+ 'evidence': '',
327
+ }
328
+
329
+ if is_vuln:
330
+ audit['vulnerable'].append(mod_id)
331
+ else:
332
+ audit['not_vuln'].append(mod_id)
333
+
334
+ # 4. Run run() — always in dry_run unless explicitly live
335
+ if is_vuln:
336
+ try:
337
+ run_fn = getattr(mod, 'run', None)
338
+ run_opts = {'dry_run': dry_run, 'attack': 'assess', 'timeout': timeout}
339
+ run_res = run_fn(host, port, **run_opts) if run_fn else {}
340
+ except Exception as exc:
341
+ run_res = {'evidence': '', 'error': str(exc)}
342
+ audit['errors'].append((mod_id, f"run() error: {exc}"))
343
+
344
+ ev = run_res.get('evidence', '') if run_res else ''
345
+ result_entry['evidence'] = ev
346
+
347
+ if verbose and ev:
348
+ for ev_line in ev.splitlines()[:12]:
349
+ lines.append(f" {_DIM}{ev_line}{_RST}")
350
+
351
+ audit['results'][mod_id] = result_entry
352
+
353
+ # ── Summary ───────────────────────────────────────────────────────────────
354
+ n_vuln = len(audit['vulnerable'])
355
+ n_safe = len(audit['not_vuln'])
356
+
357
+ lines += [
358
+ _sep('═'),
359
+ f"\n {_BLD}SUMMARY{_RST}",
360
+ f" Modules checked : {audit['total']}",
361
+ f" {_RED}Vulnerable : {n_vuln}{_RST}",
362
+ f" {_GRN}Not vulnerable : {n_safe}{_RST}",
363
+ f" Errors : {len(audit['errors'])}",
364
+ ]
365
+
366
+ if audit['vulnerable']:
367
+ lines.append(f"\n {_RED}{_BLD}DESTRUCTIVE ATTACK VECTORS CONFIRMED:{_RST}")
368
+ for vid in audit['vulnerable']:
369
+ # Find label for this id
370
+ entry = next((e for e in DESTRUCTIVE_MODULES if e[0] == vid), None)
371
+ if entry:
372
+ dc = _DAMAGE_CLR.get(entry[4], _DIM)
373
+ lines.append(f" {_RED}►{_RST} {entry[3]} {dc}[{entry[4]}]{_RST}")
374
+
375
+ if not dry_run and audit['vulnerable']:
376
+ lines += [
377
+ f"\n {_RED}{_BLD}LIVE ATTACKS DISPATCHED. Hardware damage is in progress.{_RST}",
378
+ f" {_RED}Monitor target physically. Ensure fire suppression is ready.{_RST}",
379
+ ]
380
+ elif dry_run and audit['vulnerable']:
381
+ lines += [
382
+ f"\n {_YEL}Re-run with --no-dry to execute live destructive attacks.{_RST}",
383
+ f" {_YEL}Authorized lab environments only.{_RST}",
384
+ ]
385
+
386
+ lines.append('')
387
+
388
+ report = '\n'.join(lines)
389
+ audit['report'] = report
390
+ return audit
391
+
392
+
393
+ # ── CLI entry (used by main.py) ────────────────────────────────────────────────
394
+ def main_destructive_audit(args) -> None:
395
+ """
396
+ Called from main.py when --destructive-audit is set.
397
+ Prints the audit report to stdout.
398
+ """
399
+ host = getattr(args, 'target', None)
400
+ if not host:
401
+ print(f"\n {_RED}[!] --destructive-audit requires a target IP/hostname{_RST}")
402
+ print(f" Usage: python src/main.py <target> --destructive-audit\n")
403
+ sys.exit(1)
404
+
405
+ dry_run = not getattr(args, 'no_dry', False)
406
+ port_raw = getattr(args, 'port_raw', None)
407
+ port_snmp = getattr(args, 'port_snmp', None)
408
+ port_http = getattr(args, 'port_http', None)
409
+ verbose = getattr(args, 'debug', False)
410
+ timeout = getattr(args, 'timeout', 10.0) if hasattr(args, 'timeout') else 10.0
411
+
412
+ selected_raw = getattr(args, 'destructive_modules', None)
413
+ selected_ids = [s.strip() for s in selected_raw.split(',')] if selected_raw else None
414
+
415
+ overrides: Dict[str, int] = {}
416
+ if port_raw: overrides['raw'] = port_raw
417
+ if port_snmp: overrides['snmp'] = port_snmp
418
+ if port_http: overrides['http'] = port_http
419
+
420
+ audit = run_destructive_audit(
421
+ host=host,
422
+ port_overrides=overrides,
423
+ dry_run=dry_run,
424
+ selected_ids=selected_ids,
425
+ timeout=timeout,
426
+ verbose=verbose,
427
+ )
428
+
429
+ print(audit['report'])
430
+ sys.exit(0 if not audit['errors'] else 1)