verify_wheel_contents.py 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. #!/usr/bin/env python3
  2. """Verify a built wheel ships the required bundled data and only the intended configuration templates.
  3. The wheel must contain the tracked configuration templates and must NOT contain a
  4. live configuration.py (which holds SECRET_KEY and database credentials), any other
  5. local configuration*.py variant, or any ldap_config*.py (which holds LDAP bind
  6. credentials). This guards against a dirty or manual build leaking secrets into a
  7. published artifact. The wheel must also ship the runtime-critical bundled data
  8. (release metadata, templates, translations, static assets, deployment examples), so
  9. a broken build fails here with a precise message instead of at smoke-test time.
  10. """
  11. import sys
  12. import zipfile
  13. from pathlib import PurePosixPath
  14. # The scan covers the entire wheel; only these two tracked templates (at netbox/<name> after the
  15. # wheel `sources = ["netbox"]` strip) are allowed to ship.
  16. ALLOWED = {
  17. 'netbox/configuration_example.py',
  18. 'netbox/configuration_testing.py',
  19. }
  20. # Runtime-critical bundled data; mirrors the force-include table in pyproject.toml.
  21. REQUIRED_FILES = {
  22. 'netbox/_data/release.yaml',
  23. 'netbox/_data/examples/gunicorn.py',
  24. 'netbox/_data/examples/netbox.service',
  25. 'netbox/_data/examples/netbox-rq.service',
  26. 'netbox/_data/examples/nginx.conf',
  27. 'netbox/_data/examples/apache.conf',
  28. 'netbox/_data/examples/netbox.env',
  29. }
  30. REQUIRED_PREFIXES = (
  31. 'netbox/_data/templates/',
  32. 'netbox/_data/translations/',
  33. 'netbox/_data/project-static/dist/',
  34. 'netbox/_data/project-static/img/',
  35. 'netbox/_data/project-static/js/',
  36. )
  37. def configuration_members(names):
  38. """Return the set of configuration*.py members anywhere inside the wheel."""
  39. members = set()
  40. for name in names:
  41. path = PurePosixPath(name)
  42. # Scan the whole wheel: any configuration*.py outside the two tracked templates, or any
  43. # ldap_config*.py at all, is a leak, wherever it sits in the archive.
  44. if path.suffix == '.py' and (path.name.startswith('configuration') or path.name.startswith('ldap_config')):
  45. members.add(name)
  46. return members
  47. def missing_runtime_data(names):
  48. """Return the sorted list of required bundled files and prefixes absent from the wheel."""
  49. missing = sorted(REQUIRED_FILES - names)
  50. missing += [prefix for prefix in REQUIRED_PREFIXES if not any(name.startswith(prefix) for name in names)]
  51. return missing
  52. def main(argv):
  53. if len(argv) != 2:
  54. print('usage: verify_wheel_contents.py <wheel>')
  55. return 2
  56. with zipfile.ZipFile(argv[1]) as archive:
  57. names = set(archive.namelist())
  58. found = configuration_members(names)
  59. missing = sorted(ALLOWED - found)
  60. unexpected = sorted(found - ALLOWED)
  61. missing_data = missing_runtime_data(names)
  62. if missing or unexpected or missing_data:
  63. print('Wheel contents are not as expected:')
  64. if missing:
  65. print(f' - missing templates: {missing}')
  66. if unexpected:
  67. print(f' - unexpected (possible secret leak): {unexpected}')
  68. if missing_data:
  69. print(f' - missing runtime data: {missing_data}')
  70. return 1
  71. print(f'OK: wheel ships the required bundled data and only the {len(ALLOWED)} configuration templates')
  72. return 0
  73. if __name__ == '__main__':
  74. sys.exit(main(sys.argv))