coretrace-python-analyzer 0.2.0__tar.gz → 0.3.0__tar.gz

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 (251) hide show
  1. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/CHANGELOG.md +16 -0
  2. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/PKG-INFO +1 -1
  3. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/docs/plugins.md +19 -5
  4. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/docs/usage.md +27 -3
  5. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/pyproject.toml +1 -1
  6. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/__init__.py +1 -1
  7. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/cache.py +10 -7
  8. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/engine.py +1 -1
  9. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/plugins/loader.py +18 -2
  10. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/taint/engine.py +16 -3
  11. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/taint/models.py +33 -12
  12. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_cache.py +20 -0
  13. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_interprocedural_taint.py +13 -2
  14. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_plugins.py +45 -0
  15. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_project.py +32 -0
  16. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_release.py +1 -1
  17. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_taint.py +69 -3
  18. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/.github/workflows/ci.yml +0 -0
  19. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/.github/workflows/release.yml +0 -0
  20. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/.gitignore +0 -0
  21. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/LICENSE +0 -0
  22. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/NOTICE +0 -0
  23. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/README.md +0 -0
  24. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/docs/architecture.md +0 -0
  25. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/examples/add.py +0 -0
  26. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/examples/canonical_symbols.py +0 -0
  27. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/__main__.py +0 -0
  28. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/abstract/__init__.py +0 -0
  29. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/abstract/constants.py +0 -0
  30. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/abstract/heap.py +0 -0
  31. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/abstract/ranges.py +0 -0
  32. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/abstract/values.py +0 -0
  33. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/analysis/__init__.py +0 -0
  34. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/analysis/manager.py +0 -0
  35. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/analysis/provider.py +0 -0
  36. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py +0 -0
  37. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/dependency/dependency_policy/plugin.toml +0 -0
  38. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/dependency/reachable_vulnerability/plugin.toml +0 -0
  39. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +0 -0
  40. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/dependency/sample_advisories/plugin.toml +0 -0
  41. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py +0 -0
  42. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml +0 -0
  43. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +0 -0
  44. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/aiohttp/aiohttp_models.py +0 -0
  45. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/aiohttp/plugin.toml +0 -0
  46. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/bottle/bottle_models.py +0 -0
  47. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/bottle/plugin.toml +0 -0
  48. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/cli/cli_models.py +0 -0
  49. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/cli/plugin.toml +0 -0
  50. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/credentials/credential_models.py +0 -0
  51. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/credentials/plugin.toml +0 -0
  52. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/db_drivers/db_driver_models.py +0 -0
  53. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/db_drivers/plugin.toml +0 -0
  54. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/django/django_models.py +0 -0
  55. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/django/plugin.toml +0 -0
  56. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/fastapi/fastapi_models.py +0 -0
  57. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/fastapi/plugin.toml +0 -0
  58. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/flask/flask_models.py +0 -0
  59. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/flask/plugin.toml +0 -0
  60. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/http_clients/http_client_models.py +0 -0
  61. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/http_clients/plugin.toml +0 -0
  62. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/python_stdlib/plugin.toml +0 -0
  63. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/python_stdlib/python_stdlib.py +0 -0
  64. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/sqlalchemy/plugin.toml +0 -0
  65. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/sqlalchemy/sqlalchemy_models.py +0 -0
  66. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/tornado/plugin.toml +0 -0
  67. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/models/tornado/tornado_models.py +0 -0
  68. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/secrets/config_secrets/config_secrets.py +0 -0
  69. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/secrets/config_secrets/plugin.toml +0 -0
  70. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/secrets/hardcoded_secrets/hardcoded_secrets.py +0 -0
  71. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/secrets/hardcoded_secrets/plugin.toml +0 -0
  72. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/security/command_injection/command_injection.py +0 -0
  73. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/security/command_injection/plugin.toml +0 -0
  74. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/security/insecure_deserialization/insecure_deserialization.py +0 -0
  75. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/security/insecure_deserialization/plugin.toml +0 -0
  76. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/security/open_redirect/open_redirect.py +0 -0
  77. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/security/open_redirect/plugin.toml +0 -0
  78. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/security/path_traversal/path_traversal.py +0 -0
  79. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/security/path_traversal/plugin.toml +0 -0
  80. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py +0 -0
  81. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/security/plaintext_credentials/plugin.toml +0 -0
  82. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/security/sql_injection/plugin.toml +0 -0
  83. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/security/sql_injection/sql_injection.py +0 -0
  84. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/security/ssrf/plugin.toml +0 -0
  85. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/security/ssrf/ssrf.py +0 -0
  86. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/security/xss/plugin.toml +0 -0
  87. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/security/xss/xss.py +0 -0
  88. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py +0 -0
  89. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/syntax/dangerous_eval/plugin.toml +0 -0
  90. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/syntax/flask_debug/flask_debug.py +0 -0
  91. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/syntax/flask_debug/plugin.toml +0 -0
  92. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/syntax/insecure_tempfile/insecure_tempfile.py +0 -0
  93. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/syntax/insecure_tempfile/plugin.toml +0 -0
  94. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/syntax/insecure_tls/insecure_tls.py +0 -0
  95. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/syntax/insecure_tls/plugin.toml +0 -0
  96. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/syntax/missing_timeout/missing_timeout.py +0 -0
  97. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/syntax/missing_timeout/plugin.toml +0 -0
  98. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/syntax/unsafe_archive/plugin.toml +0 -0
  99. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/syntax/unsafe_archive/unsafe_archive.py +0 -0
  100. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/syntax/unsafe_xml/plugin.toml +0 -0
  101. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/syntax/unsafe_xml/unsafe_xml.py +0 -0
  102. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/syntax/weak_crypto/plugin.toml +0 -0
  103. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/syntax/weak_crypto/weak_crypto.py +0 -0
  104. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/syntax/weak_random/plugin.toml +0 -0
  105. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/bundled/syntax/weak_random/weak_random.py +0 -0
  106. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/cfg/__init__.py +0 -0
  107. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/cfg/builder.py +0 -0
  108. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/cfg/dominance.py +0 -0
  109. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/cfg/model.py +0 -0
  110. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/cli.py +0 -0
  111. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/dataflow/__init__.py +0 -0
  112. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/dataflow/lattice.py +0 -0
  113. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/dataflow/solver.py +0 -0
  114. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/dependency/__init__.py +0 -0
  115. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/dependency/advisories.py +0 -0
  116. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/dependency/correlation.py +0 -0
  117. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/dependency/graph.py +0 -0
  118. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/dependency/policy.py +0 -0
  119. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/dependency/sbom.py +0 -0
  120. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/findings/__init__.py +0 -0
  121. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/findings/baseline.py +0 -0
  122. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/findings/coverage.py +0 -0
  123. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/findings/model.py +0 -0
  124. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/findings/refutation.py +0 -0
  125. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/findings/suppressions.py +0 -0
  126. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/frontend/__init__.py +0 -0
  127. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/frontend/ast_adapter.py +0 -0
  128. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/frontend/parser.py +0 -0
  129. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/hir/__init__.py +0 -0
  130. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/hir/nodes.py +0 -0
  131. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/hir/visitors.py +0 -0
  132. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/interprocedural/__init__.py +0 -0
  133. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/interprocedural/callgraph.py +0 -0
  134. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/interprocedural/modulegraph.py +0 -0
  135. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/interprocedural/summaries.py +0 -0
  136. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/ir/__init__.py +0 -0
  137. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/ir/defuse.py +0 -0
  138. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/ir/lowering.py +0 -0
  139. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/ir/model.py +0 -0
  140. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/ir/printer.py +0 -0
  141. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/ir/ssa.py +0 -0
  142. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/plugins/__init__.py +0 -0
  143. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/plugins/api.py +0 -0
  144. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/plugins/detectors.py +0 -0
  145. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/plugins/manifest.py +0 -0
  146. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/plugins/registry.py +0 -0
  147. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/plugins/secrets.py +0 -0
  148. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/reporters/__init__.py +0 -0
  149. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/reporters/json_format.py +0 -0
  150. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/reporters/report.py +0 -0
  151. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/reporters/sarif.py +0 -0
  152. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/reporters/text.py +0 -0
  153. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/semantic/__init__.py +0 -0
  154. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/semantic/identity.py +0 -0
  155. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/semantic/imports.py +0 -0
  156. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/semantic/scopes.py +0 -0
  157. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/semantic/symbols.py +0 -0
  158. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/source/__init__.py +0 -0
  159. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/source/manager.py +0 -0
  160. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/source/model.py +0 -0
  161. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/taint/__init__.py +0 -0
  162. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/src/coretrace_python/taint/routes.py +0 -0
  163. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/expected/Chatbot.json +0 -0
  164. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/expected/DjangoGoat.json +0 -0
  165. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/expected/Flask-Image-Editing-Website.json +0 -0
  166. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/expected/InsecureGram.json +0 -0
  167. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/expected/MiniBookApiServer.json +0 -0
  168. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/expected/NPL-IA.json +0 -0
  169. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/expected/Spam-Detection-Project.json +0 -0
  170. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/expected/Vulnerable_fastapi.json +0 -0
  171. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/expected/dvpwa.json +0 -0
  172. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/expected/full-stack-fastapi-template.json +0 -0
  173. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/expected/healthchecks.json +0 -0
  174. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/expected/httpie.json +0 -0
  175. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/expected/luigi.json +0 -0
  176. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/expected/my_gdb.json +0 -0
  177. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/expected/network-topologer.json +0 -0
  178. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/expected/pygoat.json +0 -0
  179. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/expected/uploadbox-cloud-storage-solution.json +0 -0
  180. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/expected/vulnerable-flask.json +0 -0
  181. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/expected/yt-fts.json +0 -0
  182. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/fixtures/vulnerable-flask/README.md +0 -0
  183. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/fixtures/vulnerable-flask/app.py +0 -0
  184. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/fixtures/vulnerable-flask/requirements.txt +0 -0
  185. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/fixtures/vulnerable-flask/vulnerable_services.py +0 -0
  186. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/regression/repositories.toml +0 -0
  187. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_advisory_feeds.py +0 -0
  188. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_aiohttp_models.py +0 -0
  189. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_analysis_manager.py +0 -0
  190. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_architecture.py +0 -0
  191. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_audit_detectors.py +0 -0
  192. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_baseline.py +0 -0
  193. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_cfg.py +0 -0
  194. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_class_keywords_with_targets.py +0 -0
  195. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_cli.py +0 -0
  196. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_closures.py +0 -0
  197. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_config_secrets_coverage.py +0 -0
  198. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_constants.py +0 -0
  199. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_correlation.py +0 -0
  200. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_credential_context.py +0 -0
  201. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_dataflow.py +0 -0
  202. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_dependencies.py +0 -0
  203. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_detectors.py +0 -0
  204. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_discovery.py +0 -0
  205. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_django_http_clients.py +0 -0
  206. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_django_routes.py +0 -0
  207. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_docs.py +0 -0
  208. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_dominance.py +0 -0
  209. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_emit_ir.py +0 -0
  210. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_exceptions.py +0 -0
  211. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_factories_match_loop_else.py +0 -0
  212. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_fail_on.py +0 -0
  213. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_fastapi_cli_precision.py +0 -0
  214. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_frameworks.py +0 -0
  215. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_frontend_gaps.py +0 -0
  216. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_heap.py +0 -0
  217. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_hex_digests.py +0 -0
  218. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_hir_builder.py +0 -0
  219. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_imports.py +0 -0
  220. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_interprocedural.py +0 -0
  221. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_ir.py +0 -0
  222. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_match_patterns.py +0 -0
  223. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_nonlocal_writeback.py +0 -0
  224. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_operator_sources.py +0 -0
  225. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_packaging.py +0 -0
  226. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_parallel.py +0 -0
  227. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_parser.py +0 -0
  228. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_plaintext_credentials.py +0 -0
  229. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_pyir_imports.py +0 -0
  230. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_real_code_precision.py +0 -0
  231. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_redirect_targets.py +0 -0
  232. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_refutation.py +0 -0
  233. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_refutation_evidence.py +0 -0
  234. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_regression.py +0 -0
  235. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_relative_paths.py +0 -0
  236. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_remaining_limits.py +0 -0
  237. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_reporters.py +0 -0
  238. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_scale.py +0 -0
  239. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_scopes.py +0 -0
  240. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_secrets_sbom.py +0 -0
  241. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_self_state.py +0 -0
  242. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_source_manager.py +0 -0
  243. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_ssa.py +0 -0
  244. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_suppressions.py +0 -0
  245. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_symbol_resolution.py +0 -0
  246. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_symbols.py +0 -0
  247. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_syntactic_rules.py +0 -0
  248. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_syntax_control_flow.py +0 -0
  249. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_syntax_coverage.py +0 -0
  250. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_syntax_expressions.py +0 -0
  251. {coretrace_python_analyzer-0.2.0 → coretrace_python_analyzer-0.3.0}/tests/test_tornado_bottle_models.py +0 -0
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 (2026-09-22)
4
+
5
+ ### Plugins
6
+
7
+ - Two plugins may declare the same symbol: an identical model is registered once and sinks merge their kinds and positions; a real conflict names both plugins instead of failing the run on the first duplicate (#92).
8
+ - A plugin's entrypoint may be a package, `<module>/__init__.py`, so a plugin can split its tables across files and import them relatively (#93).
9
+ - The cache key covers every file of a plugin directory, not only `.py` and `.toml`, so rules or advisories shipped as data invalidate cached results when they change (#94).
10
+
11
+ ### Analysis
12
+
13
+ - New taint kinds: `NOSQL` in `TaintKind.ALL`; `LOG` and `PII` outside it, carried only by the values a model marks, so a NoSQL-injection, log-injection or personal-data detector no longer has to share a kind with another rule (#96).
14
+
15
+ ### Precision
16
+
17
+ - A `Sanitizer` declared on a function of the analysed project takes precedence over the summary derived from its body, in the same file and across files, so a code base can declare its own validation layer (#95).
18
+
3
19
  ## 0.2.0 (2026-09-06)
4
20
 
5
21
  ### Adoption in an existing repository
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: coretrace-python-analyzer
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Static security analysis infrastructure for Python code
5
5
  Project-URL: Homepage, https://github.com/CoreTrace/coretrace-python-analyzer
6
6
  Project-URL: Repository, https://github.com/CoreTrace/coretrace-python-analyzer
@@ -38,7 +38,12 @@ class = "SensitiveLoggingPlugin"
38
38
  | `plugin_api` | The range of plugin API versions the plugin supports. The current API is version 1. |
39
39
  | `requires` | Capabilities the plugin needs; a mismatch with the class's `requires` is a manifest error. |
40
40
  | `provides` | Capabilities the plugin offers, for documentation and conflict detection. |
41
- | `entrypoint` | The module file (without `.py`, next to the manifest) and the class in it. |
41
+ | `entrypoint` | The module (without `.py`, next to the manifest) and the class in it. |
42
+
43
+ The entrypoint module may be a package, `<module>/__init__.py`, when a plugin grows past
44
+ one file: its submodules are reached through relative imports (`from .tables import
45
+ celery`). Two plugins may use the same module name; each is imported under a name derived
46
+ from its path.
42
47
 
43
48
  ## Symbols
44
49
 
@@ -101,10 +106,14 @@ class CommandInjectionPlugin(TaintDetector):
101
106
  ```
102
107
 
103
108
  `confidence` defaults to high; a hotspot, a flow behind a guard that mentions the value
104
- or an authorization decorator, is reported at medium. The taint kinds are `SQL`,
105
- `COMMAND`, `HTML`, `PATH`, `SSRF`, `CODE`, `DESERIALIZATION`, `REDIRECT`, `ADVISORY`
106
- (APIs affected by advisories) and `CREDENTIAL`, which only credential-named parameters
107
- carry so that ordinary input reaching a database write is not a plaintext credential.
109
+ or an authorization decorator, is reported at medium. The taint kinds every source
110
+ carries by default (`TaintKind.ALL`) are `SQL`, `NOSQL`, `COMMAND`, `HTML`, `PATH`,
111
+ `SSRF`, `CODE`, `DESERIALIZATION`, `REDIRECT` and `ADVISORY` (APIs affected by
112
+ advisories). Three kinds are outside `ALL`, so only a value a model marks with them
113
+ reaches their sinks: `CREDENTIAL`, which credential-named parameters carry so that
114
+ ordinary input reaching a database write is not a plaintext credential; `LOG`, for
115
+ values that must not be written to a log as they are; and `PII`, for personal data
116
+ that must not leave the application through a log, a third party or plain storage.
108
117
 
109
118
  ### Security models: `ModelPlugin`
110
119
 
@@ -142,6 +151,11 @@ class BottleModels(ModelPlugin):
142
151
  | `Validator(symbol, kinds=ALL, argument=0)` | A callable whose truth proves its argument safe, such as `re.fullmatch`; a flow guarded by it is refuted. |
143
152
  | `AuthorizationGuard(symbol, label)` | A decorator or a condition restricting who reaches the code, such as `login_required`; a flow behind it is a hotspot. |
144
153
 
154
+ Two plugins may describe the same symbol. An identical model is registered once; two
155
+ `Sink` models merge their kinds and positions, so a plugin can add `SQL` to a sink
156
+ another plugin declared for `COMMAND`; any other difference is a conflict that stops
157
+ the analysis with a `ModelError` naming both plugins.
158
+
145
159
  A model plugin may also carry `advisories`, a tuple of `Advisory` values
146
160
  (`coretrace_python.dependency`) with the package, the vulnerable range and the affected
147
161
  symbols, as the shipped `sample-advisories` plugin does. Requirements matching them are
@@ -198,7 +198,7 @@ the directory of the checked file; a path outside it is printed as it is.
198
198
  ```json
199
199
  {
200
200
  "schema_version": 1,
201
- "tool": {"name": "coretrace-python-analyzer", "version": "0.2.0"},
201
+ "tool": {"name": "coretrace-python-analyzer", "version": "0.3.0"},
202
202
  "root": "/home/me/project",
203
203
  "findings": [
204
204
  {
@@ -283,8 +283,9 @@ coretrace-python-analyzer --check src/ --sbom sbom.json --policy security/policy
283
283
  ## Large projects
284
284
 
285
285
  `--cache DIR` keeps the results of a directory check on disk, one entry per module,
286
- keyed by the module's source, the tool and plugin versions, the security models, the
287
- advisories, the dependency graph and the modules it imports. On the next run an unchanged
286
+ keyed by the module's source, the tool version, every file of every plugin directory
287
+ (code, manifest and data alike), the security models, the advisories, the dependency
288
+ graph and the modules it imports. On the next run an unchanged
288
289
  module is served from the cache, so editing one file re-analyses that file and its
289
290
  importers only. Entries are plain data; an unreadable entry is recomputed.
290
291
 
@@ -335,6 +336,29 @@ coretrace-python-analyzer --check src/ --plugins ./coretrace-plugins
335
336
  coretrace-python-analyzer --check src/ --no-bundled-plugins --plugins ./coretrace-plugins
336
337
  ```
337
338
 
339
+ ### Your own validation layer
340
+
341
+ A code base that already validates its input has functions the analyzer cannot know are
342
+ safe: a `clean()` that returns its argument looks, from its body, like a function that
343
+ passes taint through. Declare them in a model plugin by their project symbol,
344
+ `python.<module>.<function>`, and the analyzer takes your word over what it derived:
345
+
346
+ ```python
347
+ from coretrace_python.plugins import ModelPlugin
348
+ from coretrace_python.semantic.symbols import SymbolId
349
+ from coretrace_python.taint import Sanitizer, TaintKind, Validator
350
+
351
+
352
+ class HouseModels(ModelPlugin):
353
+ name = "house-models"
354
+ models = (
355
+ # the result of app.validation.clean is safe for commands, whatever the body does
356
+ Sanitizer(SymbolId("python.app.validation.clean"), TaintKind.COMMAND),
357
+ # a flow behind ``if app.validation.is_slug(value):`` is refuted
358
+ Validator(SymbolId("python.app.validation.is_slug")),
359
+ )
360
+ ```
361
+
338
362
  ## Continuous integration
339
363
 
340
364
  The exit status gates the job and the SARIF report feeds code scanning. On GitHub
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "coretrace-python-analyzer"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Static security analysis infrastructure for Python code"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,4 +1,4 @@
1
1
  """CoreTrace's Python static analysis frontend."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"
4
4
 
@@ -14,7 +14,7 @@ from __future__ import annotations
14
14
  import hashlib
15
15
  import json
16
16
  import os
17
- from collections.abc import Iterable, Mapping
17
+ from collections.abc import Mapping
18
18
  from dataclasses import dataclass
19
19
  from pathlib import Path
20
20
  from types import MappingProxyType
@@ -64,15 +64,18 @@ def fingerprint(*parts: str) -> str:
64
64
  return digest.hexdigest()
65
65
 
66
66
 
67
- def directory_fingerprint(directory: Path, suffixes: Iterable[str] = (".py", ".toml")) -> str:
68
- """A digest of the source files under ``directory``, so edited plugin code misses."""
67
+ def directory_fingerprint(directory: Path) -> str:
68
+ """A digest of every file under ``directory``, so an edited plugin misses: its code,
69
+ its manifest and the rule or advisory data it ships in any format. Bytecode and
70
+ hidden files are not part of what the plugin does."""
69
71
 
70
- wanted = tuple(suffixes)
71
72
  parts: list[str] = []
72
73
  for path in sorted(directory.rglob("*")):
73
- if path.is_file() and path.suffix in wanted:
74
- parts.append(str(path.relative_to(directory)))
75
- parts.append(path.read_text(encoding="utf-8", errors="replace"))
74
+ relative = path.relative_to(directory)
75
+ if not path.is_file() or any(p == "__pycache__" or p.startswith(".") for p in relative.parts):
76
+ continue
77
+ parts.append(str(relative))
78
+ parts.append(path.read_bytes().hex())
76
79
  return fingerprint(*parts)
77
80
 
78
81
 
@@ -257,7 +257,7 @@ def load_plugins(plugin_roots: Sequence[Path], manager: AnalysisManager) -> Plug
257
257
  def plugin_models(plugins: Iterable[Plugin]) -> ModelTable:
258
258
  models = SecurityModelRegistry()
259
259
  for plugin in plugins:
260
- models.register(*plugin.models)
260
+ models.register(*plugin.models, origin=plugin.name)
261
261
  return models.freeze()
262
262
 
263
263
 
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import hashlib
6
6
  import importlib.util
7
+ import sys
7
8
  from dataclasses import dataclass
8
9
  from pathlib import Path
9
10
 
@@ -63,16 +64,31 @@ def discover_plugins(root: Path, manager: AnalysisManager) -> tuple[LoadedPlugin
63
64
 
64
65
 
65
66
  def _import_entrypoint(directory: Path, manifest: PluginManifest) -> type[Plugin]:
66
- module_path = directory / f"{manifest.entrypoint.module}.py"
67
+ """Import the entrypoint, a module ``<name>.py`` or a package ``<name>/__init__.py``.
68
+
69
+ The module name derives from the path, so two plugins may both call their module
70
+ ``models``; a package is registered in ``sys.modules`` under that name before it
71
+ runs, which is what its relative imports resolve through."""
72
+
73
+ package_path = directory / manifest.entrypoint.module / "__init__.py"
74
+ is_package = package_path.is_file()
75
+ module_path = package_path if is_package else directory / f"{manifest.entrypoint.module}.py"
67
76
  if not module_path.is_file():
68
77
  raise ManifestError(
69
78
  f"{directory / MANIFEST_FILENAME}: entrypoint module"
70
79
  f" {manifest.entrypoint.module!r} not found"
71
80
  )
72
81
  unique = hashlib.sha1(str(module_path.resolve()).encode()).hexdigest()[:12]
73
- spec = importlib.util.spec_from_file_location(f"coretrace_plugin_{unique}", module_path)
82
+ name = f"coretrace_plugin_{unique}"
83
+ spec = importlib.util.spec_from_file_location(
84
+ name,
85
+ module_path,
86
+ submodule_search_locations=[str(module_path.parent)] if is_package else None,
87
+ )
74
88
  assert spec is not None and spec.loader is not None
75
89
  module = importlib.util.module_from_spec(spec)
90
+ if is_package:
91
+ sys.modules[name] = module
76
92
  spec.loader.exec_module(module)
77
93
 
78
94
  candidate = getattr(module, manifest.entrypoint.class_name, None)
@@ -147,8 +147,10 @@ class _TaintProblem(DataflowProblem[State]):
147
147
  project: SummaryIndex | None = None,
148
148
  heap: HeapFacts | None = None,
149
149
  seeds: Mapping[HeapLocation, Taint] | None = None,
150
+ module: str = "",
150
151
  ) -> None:
151
152
  self.name = name
153
+ self.module = module
152
154
  self.function = function
153
155
  self.models = models
154
156
  self.graph = graph
@@ -294,9 +296,10 @@ class _TaintProblem(DataflowProblem[State]):
294
296
  through = target.symbol.canonical_name.removeprefix("python.")
295
297
  bound = () if project.static else self.receiver(call, project.name)
296
298
  arguments = (*(self.deep(r, state) for r in bound), *arguments)
297
- return self.known(
299
+ result = self.known(
298
300
  project, through, arguments, keywords, everything, call, flows, state, (), bound
299
301
  )
302
+ return self.declared_clean(target.symbol, result)
300
303
  symbol = target.symbol
301
304
  if not self.modelled(symbol):
302
305
  # ``get_conn().execute`` derived ``app.database.get_conn.execute``; what
@@ -312,9 +315,10 @@ class _TaintProblem(DataflowProblem[State]):
312
315
  *arguments,
313
316
  *(self.deep(value, state) for value in captured),
314
317
  )
315
- return self.known(
318
+ result = self.known(
316
319
  summary, target.name, arguments, keywords, everything, call, flows, state, captured, bound
317
320
  )
321
+ return self.declared_clean(project_symbol(self.module, target.name), result)
318
322
  returned = self.returned_symbol(call)
319
323
  if returned is not None:
320
324
  return self.external(returned, everything, call, state, flows)
@@ -369,6 +373,13 @@ class _TaintProblem(DataflowProblem[State]):
369
373
  made = self.defs.get(call.callee)
370
374
  return made.captured if isinstance(made, MakeFunction) else ()
371
375
 
376
+ def declared_clean(self, symbol: SymbolId, result: Taint) -> Taint:
377
+ """A ``Sanitizer`` declared on a project function is the project's own knowledge
378
+ of what its result carries; it wins over what the summary derived from the body."""
379
+
380
+ sanitizer = self.models.sanitizer(symbol)
381
+ return result if sanitizer is None else result.without(sanitizer.kinds)
382
+
372
383
  def modelled(self, symbol: SymbolId) -> bool:
373
384
  return (
374
385
  self.models.sink(symbol) is not None
@@ -730,8 +741,9 @@ def propagate_taint(
730
741
  project: SummaryIndex | None = None,
731
742
  heap: HeapFacts | None = None,
732
743
  seeds: Mapping[HeapLocation, Taint] | None = None,
744
+ module: str = "",
733
745
  ) -> TaintFacts:
734
- problem = _TaintProblem(name, function, models, graph, summaries, parameters, project, heap, seeds)
746
+ problem = _TaintProblem(name, function, models, graph, summaries, parameters, project, heap, seeds, module)
735
747
  solution = solve(problem, cfg)
736
748
  taints: dict[Key, Taint] = {}
737
749
  flows: list[TaintFlow] = []
@@ -794,6 +806,7 @@ class TaintAnalysis(FunctionAnalysis[TaintFacts]):
794
806
  ctx.get(ProjectSummaries),
795
807
  heap,
796
808
  seeds,
809
+ ctx.module.name,
797
810
  )
798
811
 
799
812
 
@@ -30,10 +30,14 @@ class TaintKind(Flag):
30
30
  ADVISORY = auto()
31
31
  DESERIALIZATION = auto()
32
32
  REDIRECT = auto()
33
- ALL = SQL | COMMAND | HTML | PATH | SSRF | CODE | ADVISORY | DESERIALIZATION | REDIRECT
34
- # Outside ALL on purpose: only credential-named parameters carry it, so a database
35
- # write reached by ordinary input is not a plaintext credential.
33
+ NOSQL = auto()
34
+ ALL = SQL | COMMAND | HTML | PATH | SSRF | CODE | ADVISORY | DESERIALIZATION | REDIRECT | NOSQL
35
+ # Outside ALL on purpose: only the values a model marks carry them, so a database
36
+ # write reached by ordinary input is not a plaintext credential, and ordinary input
37
+ # reaching a logger is neither log forging nor a personal-data leak.
36
38
  CREDENTIAL = auto()
39
+ LOG = auto()
40
+ PII = auto()
37
41
 
38
42
 
39
43
  class ModelError(Exception):
@@ -69,6 +73,11 @@ class Sink:
69
73
  kinds &= ~restricted
70
74
  return kinds
71
75
 
76
+ def merged(self, other: Sink) -> Sink:
77
+ """This sink with ``other``'s kinds and positions added."""
78
+
79
+ return Sink(self.symbol, self.kinds | other.kinds, self.positions + other.positions)
80
+
72
81
 
73
82
  @dataclass(frozen=True)
74
83
  class Sanitizer:
@@ -251,11 +260,7 @@ class ModelTable:
251
260
  merged = {sink.symbol: sink for sink in self.sinks}
252
261
  for sink in sinks:
253
262
  current = merged.get(sink.symbol)
254
- merged[sink.symbol] = (
255
- Sink(sink.symbol, current.kinds | sink.kinds, current.positions + sink.positions)
256
- if current is not None
257
- else sink
258
- )
263
+ merged[sink.symbol] = sink if current is None else current.merged(sink)
259
264
  return ModelTable(
260
265
  self.sources,
261
266
  tuple(merged.values()),
@@ -279,15 +284,31 @@ class SecurityModelRegistry:
279
284
 
280
285
  def __init__(self) -> None:
281
286
  self._models: dict[tuple[type[Model], SymbolId], Model] = {}
287
+ self._origins: dict[tuple[type[Model], SymbolId], str | None] = {}
288
+
289
+ def register(self, *models: Model, origin: str | None = None) -> None:
290
+ """Add models; ``origin`` names the plugin so a conflict can name both sides.
291
+
292
+ Two plugins may describe the same symbol: an identical model is ignored, sinks
293
+ merge their kinds and positions, any other difference is a conflict."""
282
294
 
283
- def register(self, *models: Model) -> None:
284
295
  for model in models:
285
296
  key = (type(model), model.symbol)
286
- if key in self._models:
297
+ current = self._models.get(key)
298
+ if current is None:
299
+ self._models[key] = model
300
+ self._origins[key] = origin
301
+ elif current == model:
302
+ continue
303
+ elif isinstance(model, Sink) and isinstance(current, Sink):
304
+ self._models[key] = current.merged(model)
305
+ else:
306
+ registered = self._origins[key]
307
+ sides = f" by {registered!r} and {origin!r}" if registered and origin else ""
287
308
  raise ModelError(
288
- f"{type(model).__name__.lower()} model for {model.symbol} is already registered"
309
+ f"{type(model).__name__.lower()} model for {model.symbol}"
310
+ f" is registered twice{sides} with different values"
289
311
  )
290
- self._models[key] = model
291
312
 
292
313
  def freeze(self) -> ModelTable:
293
314
  models = list(self._models.values())
@@ -13,6 +13,7 @@ Expected to remain red until ``coretrace_python.cache`` and the ``--cache`` opti
13
13
  from __future__ import annotations
14
14
 
15
15
  import json
16
+ import shutil
16
17
  from pathlib import Path
17
18
 
18
19
  import pytest
@@ -119,6 +120,25 @@ def test_keys_depend_on_dependency_files(tmp_path: Path) -> None:
119
120
  assert all(after[name] != before[name] for name in before)
120
121
 
121
122
 
123
+ def test_keys_depend_on_every_plugin_file(tmp_path: Path) -> None:
124
+ root = standard(tmp_path)
125
+ plugins = tmp_path / "plugins"
126
+ shutil.copytree(
127
+ PLUGINS / "syntax" / "dangerous_eval",
128
+ plugins / "dangerous_eval",
129
+ ignore=shutil.ignore_patterns("__pycache__"),
130
+ )
131
+ before = engine.analyze_project(root, [plugins]).keys
132
+ (plugins / "dangerous_eval" / "rules.json").write_text('{"v": 1}', encoding="utf-8")
133
+ after = engine.analyze_project(root, [plugins]).keys
134
+ (plugins / "dangerous_eval" / "__pycache__").mkdir(exist_ok=True)
135
+ (plugins / "dangerous_eval" / "__pycache__" / "x.pyc").write_bytes(b"\0")
136
+ with_bytecode = engine.analyze_project(root, [plugins]).keys
137
+
138
+ assert all(after[name] != before[name] for name in before)
139
+ assert with_bytecode == after
140
+
141
+
122
142
  # --------------------------------------------------------------------------- reuse
123
143
 
124
144
 
@@ -50,12 +50,12 @@ def require_interprocedural_taint() -> None:
50
50
  pytest.fail("TaintAnalysis does not consume the call graph and summaries yet")
51
51
 
52
52
 
53
- def facts(source_text: str, name: str = "run") -> TaintFacts:
53
+ def facts(source_text: str, name: str = "run", *extra: Sanitizer) -> TaintFacts:
54
54
  module = build_hir(SourceManager().add_source("ip.py", source_text))
55
55
  manager = AnalysisManager(module)
56
56
  manager.register(*engine.ALL_ANALYSES)
57
57
  registry = SecurityModelRegistry()
58
- registry.register(*MODELS)
58
+ registry.register(*MODELS, *extra)
59
59
  manager.provide(SecurityModelAnalysis, registry.freeze())
60
60
  function = next(s for s in module.body if isinstance(s, nodes.Function) and s.name == name)
61
61
  return manager.get(TaintAnalysis, function)
@@ -130,6 +130,17 @@ def test_sanitizing_before_the_call_removes_the_flow() -> None:
130
130
  assert result.flows == ()
131
131
 
132
132
 
133
+ def test_a_sanitizer_declared_on_a_project_function_overrides_its_summary() -> None:
134
+ source = (
135
+ "import os\n\n"
136
+ "def clean(value):\n return value\n\n"
137
+ "def run():\n os.system(clean(input()))\n"
138
+ )
139
+
140
+ assert lines(facts(source)) == [7]
141
+ assert facts(source, "run", Sanitizer(SymbolId("python.ip.clean"), TaintKind.COMMAND)).flows == ()
142
+
143
+
133
144
  def test_keyword_arguments_flow_through_summaries() -> None:
134
145
  result = facts(
135
146
  "import subprocess\n\n"
@@ -357,6 +357,51 @@ def test_discovers_plugins_recursively_in_sorted_order(tmp_path: Path) -> None:
357
357
  assert [plugin.manifest.name for plugin in loaded] == ["first", "second"]
358
358
 
359
359
 
360
+ def write_package(directory: Path, label: str) -> None:
361
+ package = directory / "sample"
362
+ (package / "tables").mkdir(parents=True)
363
+ (package / "__init__.py").write_text(
364
+ "from typing import ClassVar\n\n"
365
+ "from coretrace_python.plugins import ModelPlugin\n\n"
366
+ "from .tables.celery import MODELS\n\n\n"
367
+ "class SamplePlugin(ModelPlugin):\n"
368
+ ' name: ClassVar[str] = "sample"\n'
369
+ " models = MODELS\n",
370
+ encoding="utf-8",
371
+ )
372
+ (package / "tables" / "__init__.py").write_text("", encoding="utf-8")
373
+ (package / "tables" / "celery.py").write_text(
374
+ "from coretrace_python.semantic.symbols import SymbolId\n"
375
+ "from coretrace_python.taint import EntryPoint\n\n"
376
+ f'MODELS = (EntryPoint(SymbolId("python.celery.{label}"), "{label}"),)\n',
377
+ encoding="utf-8",
378
+ )
379
+
380
+
381
+ def test_loads_a_plugin_package_with_relative_imports(tmp_path: Path) -> None:
382
+ write_manifest(tmp_path)
383
+ write_package(tmp_path, "task")
384
+
385
+ loaded = load_plugin(tmp_path, manager_for(""))
386
+
387
+ assert [m.symbol for m in loaded.plugin.models] == [SymbolId("python.celery.task")]
388
+
389
+
390
+ def test_plugin_packages_with_the_same_module_name_do_not_collide(tmp_path: Path) -> None:
391
+ for name in ("a", "b"):
392
+ directory = tmp_path / name
393
+ directory.mkdir()
394
+ write_manifest(directory, name=name)
395
+ write_package(directory, name)
396
+
397
+ loaded = discover_plugins(tmp_path, manager_for(""))
398
+
399
+ assert [m.symbol for p in loaded for m in p.plugin.models] == [
400
+ SymbolId("python.celery.a"),
401
+ SymbolId("python.celery.b"),
402
+ ]
403
+
404
+
360
405
  # --------------------------------------------------------------------------- registry
361
406
 
362
407
 
@@ -136,6 +136,38 @@ def test_taint_flows_into_a_function_defined_in_another_file(tmp_path: Path) ->
136
136
  assert findings[0].metadata["sink_line"] == "4"
137
137
 
138
138
 
139
+ def test_a_sanitizer_declared_on_a_function_of_another_file_refutes_the_flow(tmp_path: Path) -> None:
140
+ root = project(
141
+ tmp_path / "src",
142
+ {
143
+ "app/__init__.py": "",
144
+ "app/validation.py": "def clean(value):\n return value\n",
145
+ "app/main.py": "import os\nfrom app.validation import clean\n\ndef run():\n os.system(clean(input()))\n",
146
+ },
147
+ )
148
+ models = project(
149
+ tmp_path / "models",
150
+ {
151
+ "plugin.toml": (
152
+ 'name = "house-models"\nversion = "1.0.0"\nplugin_api = ">=1,<2"\n'
153
+ 'requires = []\nprovides = ["models.house"]\n\n'
154
+ '[entrypoint]\nmodule = "house"\nclass = "HouseModels"\n'
155
+ ),
156
+ "house.py": (
157
+ "from coretrace_python.plugins import ModelPlugin\n"
158
+ "from coretrace_python.semantic.symbols import SymbolId\n"
159
+ "from coretrace_python.taint import Sanitizer, TaintKind\n\n\n"
160
+ "class HouseModels(ModelPlugin):\n"
161
+ ' name = "house-models"\n'
162
+ ' models = (Sanitizer(SymbolId("python.app.validation.clean"), TaintKind.COMMAND),)\n'
163
+ ),
164
+ },
165
+ )
166
+
167
+ assert rules(engine.analyze_project(root, [PLUGINS]).findings) == [("main.py", "command-injection", 5)]
168
+ assert rules(engine.analyze_project(root, [PLUGINS, models]).findings) == []
169
+
170
+
139
171
  def test_module_attribute_calls_resolve_too(tmp_path: Path) -> None:
140
172
  root = project(
141
173
  tmp_path,
@@ -30,7 +30,7 @@ def require_changelog() -> None:
30
30
  def test_the_version_is_declared_once_and_mirrored() -> None:
31
31
  project = tomllib.loads((REPO / "pyproject.toml").read_text(encoding="utf-8"))["project"]
32
32
 
33
- assert project["version"] == coretrace_python.__version__ == "0.2.0"
33
+ assert project["version"] == coretrace_python.__version__ == "0.3.0"
34
34
 
35
35
 
36
36
  def test_the_changelog_starts_with_the_current_version() -> None:
@@ -101,6 +101,38 @@ def test_taint_kinds_form_a_bitset() -> None:
101
101
  assert {TaintKind.SQL, TaintKind.COMMAND, TaintKind.HTML, TaintKind.PATH, TaintKind.SSRF} <= set(TaintKind)
102
102
 
103
103
 
104
+ def test_nosql_is_ordinary_input_while_log_and_pii_are_opt_in() -> None:
105
+ assert TaintKind.NOSQL in TaintKind.ALL
106
+ assert TaintKind.LOG not in TaintKind.ALL
107
+ assert TaintKind.PII not in TaintKind.ALL
108
+ assert TaintKind.CREDENTIAL not in TaintKind.ALL
109
+
110
+
111
+ def test_ordinary_input_reaches_a_nosql_sink_but_not_a_log_sink() -> None:
112
+ registry = SecurityModelRegistry()
113
+ registry.register(
114
+ Source(sym("python.flask.request.args"), HTTP),
115
+ Source(sym("python.app.record"), "record", TaintKind.LOG),
116
+ Sink(sym("python.db.find"), TaintKind.NOSQL),
117
+ Sink(sym("python.logging.info"), TaintKind.LOG),
118
+ )
119
+ _, facts = analyze(
120
+ "from flask import request\n"
121
+ "import db, logging\n"
122
+ "from app import record\n\n"
123
+ "def run():\n"
124
+ " db.find(request.args['q'])\n"
125
+ " logging.info(request.args['q'])\n"
126
+ " logging.info(record())\n",
127
+ registry.freeze(),
128
+ )
129
+
130
+ assert [(f.sink.symbol, f.kinds) for f in facts.flows] == [
131
+ (sym("python.db.find"), TaintKind.NOSQL),
132
+ (sym("python.logging.info"), TaintKind.LOG),
133
+ ]
134
+
135
+
104
136
  def test_taint_joins_with_or() -> None:
105
137
  a = Taint(TaintKind.SQL, frozenset({Source(sym("python.a"), "a")}))
106
138
  b = Taint(TaintKind.HTML, frozenset({Source(sym("python.b"), "b")}))
@@ -131,10 +163,44 @@ def test_registry_indexes_models_by_symbol() -> None:
131
163
 
132
164
  def test_registry_rejects_conflicting_models() -> None:
133
165
  registry = SecurityModelRegistry()
134
- registry.register(Sink(sym("python.os.system"), TaintKind.COMMAND))
166
+ registry.register(Source(sym("python.flask.request.args"), "http"))
167
+
168
+ with pytest.raises(ModelError, match="python.flask.request.args"):
169
+ registry.register(Source(sym("python.flask.request.args"), "cli"))
170
+
171
+
172
+ def test_registry_ignores_an_identical_model() -> None:
173
+ registry = SecurityModelRegistry()
174
+ registry.register(Sanitizer(sym("python.html.escape"), TaintKind.HTML))
175
+ registry.register(Sanitizer(sym("python.html.escape"), TaintKind.HTML))
176
+
177
+ assert registry.freeze().sanitizers == (Sanitizer(sym("python.html.escape"), TaintKind.HTML),)
178
+
179
+
180
+ def test_registry_merges_sinks_on_one_symbol() -> None:
181
+ registry = SecurityModelRegistry()
182
+ registry.register(Sink(sym("python.os.system"), TaintKind.COMMAND, ((TaintKind.COMMAND, (0,)),)))
183
+ registry.register(Sink(sym("python.os.system"), TaintKind.SQL))
184
+
185
+ sink = registry.freeze().sink(sym("python.os.system"))
186
+ assert sink == Sink(
187
+ sym("python.os.system"), TaintKind.COMMAND | TaintKind.SQL, ((TaintKind.COMMAND, (0,)),)
188
+ )
189
+
190
+
191
+ def test_plugin_models_names_both_plugins_of_a_conflict() -> None:
192
+ from coretrace_python.plugins import ModelPlugin
193
+
194
+ class First(ModelPlugin):
195
+ name = "first-models"
196
+ models = (Source(sym("python.flask.request.args"), "http"),)
197
+
198
+ class Second(ModelPlugin):
199
+ name = "second-models"
200
+ models = (Source(sym("python.flask.request.args"), "cli"),)
135
201
 
136
- with pytest.raises(ModelError, match="python.os.system"):
137
- registry.register(Sink(sym("python.os.system"), TaintKind.SQL))
202
+ with pytest.raises(ModelError, match="'first-models' and 'second-models'"):
203
+ engine.plugin_models([First(), Second()])
138
204
 
139
205
 
140
206
  def test_models_and_tables_are_immutable() -> None: