coretrace-python-analyzer 0.14.0__tar.gz → 0.15.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 (283) hide show
  1. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/CHANGELOG.md +10 -0
  2. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/PKG-INFO +1 -1
  3. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/docs/plugins.md +3 -1
  4. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/docs/usage.md +21 -4
  5. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/pyproject.toml +1 -1
  6. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/__init__.py +1 -1
  7. coretrace_python_analyzer-0.15.0/src/coretrace_python/bundled/security/open_redirect/open_redirect.py +47 -0
  8. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/open_redirect/plugin.toml +1 -1
  9. coretrace_python_analyzer-0.15.0/src/coretrace_python/bundled/security/ssrf/ssrf.py +49 -0
  10. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dependency/advisories.py +6 -1
  11. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dependency/correlation.py +36 -5
  12. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dependency/graph.py +4 -2
  13. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/engine.py +7 -6
  14. coretrace_python_analyzer-0.15.0/src/coretrace_python/taint/urls.py +70 -0
  15. coretrace_python_analyzer-0.15.0/tests/test_advisory_url_host.py +145 -0
  16. coretrace_python_analyzer-0.15.0/tests/test_open_redirect_destinations.py +116 -0
  17. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_release.py +1 -1
  18. coretrace_python_analyzer-0.15.0/tests/test_url_proof.py +80 -0
  19. coretrace_python_analyzer-0.14.0/src/coretrace_python/bundled/security/open_redirect/open_redirect.py +0 -17
  20. coretrace_python_analyzer-0.14.0/src/coretrace_python/bundled/security/ssrf/ssrf.py +0 -64
  21. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/.github/workflows/ci.yml +0 -0
  22. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/.github/workflows/release.yml +0 -0
  23. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/.gitignore +0 -0
  24. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/LICENSE +0 -0
  25. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/NOTICE +0 -0
  26. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/README.md +0 -0
  27. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/docs/architecture.md +0 -0
  28. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/examples/add.py +0 -0
  29. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/examples/canonical_symbols.py +0 -0
  30. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/__main__.py +0 -0
  31. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/abstract/__init__.py +0 -0
  32. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/abstract/constants.py +0 -0
  33. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/abstract/heap.py +0 -0
  34. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/abstract/ranges.py +0 -0
  35. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/abstract/strings.py +0 -0
  36. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/abstract/values.py +0 -0
  37. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/analysis/__init__.py +0 -0
  38. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/analysis/manager.py +0 -0
  39. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/analysis/provider.py +0 -0
  40. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py +0 -0
  41. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/dependency/dependency_policy/plugin.toml +0 -0
  42. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/dependency/reachable_vulnerability/plugin.toml +0 -0
  43. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +0 -0
  44. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/dependency/sample_advisories/plugin.toml +0 -0
  45. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py +0 -0
  46. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml +0 -0
  47. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +0 -0
  48. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/aiohttp/aiohttp_models.py +0 -0
  49. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/aiohttp/plugin.toml +0 -0
  50. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/bottle/bottle_models.py +0 -0
  51. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/bottle/plugin.toml +0 -0
  52. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/cli/cli_models.py +0 -0
  53. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/cli/plugin.toml +0 -0
  54. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/credentials/credential_models.py +0 -0
  55. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/credentials/plugin.toml +0 -0
  56. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/db_drivers/db_driver_models.py +0 -0
  57. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/db_drivers/plugin.toml +0 -0
  58. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/django/django_models.py +0 -0
  59. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/django/plugin.toml +0 -0
  60. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/fastapi/fastapi_models.py +0 -0
  61. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/fastapi/plugin.toml +0 -0
  62. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/flask/flask_models.py +0 -0
  63. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/flask/plugin.toml +0 -0
  64. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/http_clients/http_client_models.py +0 -0
  65. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/http_clients/plugin.toml +0 -0
  66. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/python_stdlib/plugin.toml +0 -0
  67. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/python_stdlib/python_stdlib.py +0 -0
  68. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/sqlalchemy/plugin.toml +0 -0
  69. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/sqlalchemy/sqlalchemy_models.py +0 -0
  70. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/tornado/plugin.toml +0 -0
  71. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/tornado/tornado_models.py +0 -0
  72. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/secrets/config_secrets/config_secrets.py +0 -0
  73. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/secrets/config_secrets/plugin.toml +0 -0
  74. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/secrets/hardcoded_secrets/hardcoded_secrets.py +0 -0
  75. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/secrets/hardcoded_secrets/plugin.toml +0 -0
  76. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/code_injection/code_injection.py +0 -0
  77. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/code_injection/plugin.toml +0 -0
  78. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/command_injection/command_injection.py +0 -0
  79. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/command_injection/plugin.toml +0 -0
  80. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/insecure_deserialization/insecure_deserialization.py +0 -0
  81. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/insecure_deserialization/plugin.toml +0 -0
  82. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/nosql_injection/nosql_injection.py +0 -0
  83. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/nosql_injection/plugin.toml +0 -0
  84. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/path_traversal/path_traversal.py +0 -0
  85. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/path_traversal/plugin.toml +0 -0
  86. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py +0 -0
  87. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/plaintext_credentials/plugin.toml +0 -0
  88. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/sql_injection/plugin.toml +0 -0
  89. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/sql_injection/sql_injection.py +0 -0
  90. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/ssrf/plugin.toml +0 -0
  91. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/xss/plugin.toml +0 -0
  92. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/xss/xss.py +0 -0
  93. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py +0 -0
  94. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/dangerous_eval/plugin.toml +0 -0
  95. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/flask_debug/flask_debug.py +0 -0
  96. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/flask_debug/plugin.toml +0 -0
  97. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/insecure_tempfile/insecure_tempfile.py +0 -0
  98. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/insecure_tempfile/plugin.toml +0 -0
  99. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/insecure_tls/insecure_tls.py +0 -0
  100. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/insecure_tls/plugin.toml +0 -0
  101. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/missing_timeout/missing_timeout.py +0 -0
  102. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/missing_timeout/plugin.toml +0 -0
  103. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/unsafe_archive/plugin.toml +0 -0
  104. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/unsafe_archive/unsafe_archive.py +0 -0
  105. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/unsafe_xml/plugin.toml +0 -0
  106. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/unsafe_xml/unsafe_xml.py +0 -0
  107. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/weak_crypto/plugin.toml +0 -0
  108. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/weak_crypto/weak_crypto.py +0 -0
  109. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/weak_random/plugin.toml +0 -0
  110. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/weak_random/weak_random.py +0 -0
  111. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/cache.py +0 -0
  112. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/cfg/__init__.py +0 -0
  113. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/cfg/builder.py +0 -0
  114. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/cfg/dominance.py +0 -0
  115. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/cfg/model.py +0 -0
  116. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/cli.py +0 -0
  117. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dataflow/__init__.py +0 -0
  118. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dataflow/lattice.py +0 -0
  119. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dataflow/solver.py +0 -0
  120. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dependency/__init__.py +0 -0
  121. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dependency/policy.py +0 -0
  122. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dependency/sbom.py +0 -0
  123. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dependency/vex.py +0 -0
  124. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/findings/__init__.py +0 -0
  125. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/findings/baseline.py +0 -0
  126. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/findings/coverage.py +0 -0
  127. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/findings/model.py +0 -0
  128. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/findings/provenance.py +0 -0
  129. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/findings/refutation.py +0 -0
  130. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/findings/suppressions.py +0 -0
  131. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/frontend/__init__.py +0 -0
  132. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/frontend/ast_adapter.py +0 -0
  133. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/frontend/parser.py +0 -0
  134. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/hir/__init__.py +0 -0
  135. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/hir/nodes.py +0 -0
  136. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/hir/visitors.py +0 -0
  137. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/interprocedural/__init__.py +0 -0
  138. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/interprocedural/callgraph.py +0 -0
  139. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/interprocedural/modulegraph.py +0 -0
  140. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/interprocedural/summaries.py +0 -0
  141. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/ir/__init__.py +0 -0
  142. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/ir/defuse.py +0 -0
  143. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/ir/lowering.py +0 -0
  144. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/ir/model.py +0 -0
  145. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/ir/printer.py +0 -0
  146. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/ir/ssa.py +0 -0
  147. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/plugins/__init__.py +0 -0
  148. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/plugins/api.py +0 -0
  149. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/plugins/detectors.py +0 -0
  150. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/plugins/loader.py +0 -0
  151. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/plugins/manifest.py +0 -0
  152. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/plugins/registry.py +0 -0
  153. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/plugins/secrets.py +0 -0
  154. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/reporters/__init__.py +0 -0
  155. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/reporters/json_format.py +0 -0
  156. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/reporters/report.py +0 -0
  157. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/reporters/sarif.py +0 -0
  158. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/reporters/text.py +0 -0
  159. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/semantic/__init__.py +0 -0
  160. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/semantic/identity.py +0 -0
  161. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/semantic/imports.py +0 -0
  162. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/semantic/scopes.py +0 -0
  163. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/semantic/symbols.py +0 -0
  164. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/source/__init__.py +0 -0
  165. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/source/manager.py +0 -0
  166. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/source/model.py +0 -0
  167. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/taint/__init__.py +0 -0
  168. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/taint/engine.py +0 -0
  169. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/taint/models.py +0 -0
  170. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/taint/routes.py +0 -0
  171. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/taint/templates.py +0 -0
  172. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/Chatbot.json +0 -0
  173. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/DjangoGoat.json +0 -0
  174. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/Flask-Image-Editing-Website.json +0 -0
  175. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/InsecureGram.json +0 -0
  176. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/MiniBookApiServer.json +0 -0
  177. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/NPL-IA.json +0 -0
  178. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/Spam-Detection-Project.json +0 -0
  179. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/Vulnerable_fastapi.json +0 -0
  180. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/dvpwa.json +0 -0
  181. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/full-stack-fastapi-template.json +0 -0
  182. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/healthchecks.json +0 -0
  183. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/httpie.json +0 -0
  184. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/luigi.json +0 -0
  185. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/my_gdb.json +0 -0
  186. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/network-topologer.json +0 -0
  187. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/pygoat.json +0 -0
  188. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/uploadbox-cloud-storage-solution.json +0 -0
  189. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/vulnerable-flask.json +0 -0
  190. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/yt-fts.json +0 -0
  191. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/fixtures/vulnerable-flask/README.md +0 -0
  192. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/fixtures/vulnerable-flask/app.py +0 -0
  193. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/fixtures/vulnerable-flask/requirements.txt +0 -0
  194. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/fixtures/vulnerable-flask/vulnerable_services.py +0 -0
  195. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/regression/repositories.toml +0 -0
  196. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_advisory_evidence.py +0 -0
  197. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_advisory_feeds.py +0 -0
  198. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_aiohttp_models.py +0 -0
  199. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_analysis_manager.py +0 -0
  200. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_architecture.py +0 -0
  201. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_argument_conditions.py +0 -0
  202. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_attacker_arguments.py +0 -0
  203. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_audit_detectors.py +0 -0
  204. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_baseline.py +0 -0
  205. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_cache.py +0 -0
  206. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_cfg.py +0 -0
  207. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_class_keywords_with_targets.py +0 -0
  208. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_cli.py +0 -0
  209. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_closures.py +0 -0
  210. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_code_injection.py +0 -0
  211. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_config_secrets_coverage.py +0 -0
  212. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_constants.py +0 -0
  213. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_correlation.py +0 -0
  214. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_credential_context.py +0 -0
  215. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_dataflow.py +0 -0
  216. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_dependencies.py +0 -0
  217. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_detectors.py +0 -0
  218. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_discovery.py +0 -0
  219. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_django_http_clients.py +0 -0
  220. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_django_routes.py +0 -0
  221. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_django_safe_values.py +0 -0
  222. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_docs.py +0 -0
  223. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_dominance.py +0 -0
  224. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_emit_ir.py +0 -0
  225. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_exceptions.py +0 -0
  226. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_factories_match_loop_else.py +0 -0
  227. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_fail_on.py +0 -0
  228. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_fastapi_cli_precision.py +0 -0
  229. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_frameworks.py +0 -0
  230. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_frontend_gaps.py +0 -0
  231. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_heap.py +0 -0
  232. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_hex_digests.py +0 -0
  233. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_hir_builder.py +0 -0
  234. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_imports.py +0 -0
  235. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_interprocedural.py +0 -0
  236. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_interprocedural_taint.py +0 -0
  237. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_ir.py +0 -0
  238. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_match_patterns.py +0 -0
  239. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_members.py +0 -0
  240. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_module_level.py +0 -0
  241. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_nonlocal_writeback.py +0 -0
  242. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_nosql_injection.py +0 -0
  243. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_operator_sources.py +0 -0
  244. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_packaging.py +0 -0
  245. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_parallel.py +0 -0
  246. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_parser.py +0 -0
  247. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_plaintext_credentials.py +0 -0
  248. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_plugins.py +0 -0
  249. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_project.py +0 -0
  250. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_project_models.py +0 -0
  251. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_project_validators.py +0 -0
  252. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_provenance.py +0 -0
  253. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_pyir_imports.py +0 -0
  254. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_read_entry_points.py +0 -0
  255. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_real_code_precision.py +0 -0
  256. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_redirect_targets.py +0 -0
  257. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_refutation.py +0 -0
  258. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_refutation_evidence.py +0 -0
  259. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_regression.py +0 -0
  260. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_relative_paths.py +0 -0
  261. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_remaining_limits.py +0 -0
  262. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_reporters.py +0 -0
  263. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_safe_arguments.py +0 -0
  264. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_scale.py +0 -0
  265. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_scopes.py +0 -0
  266. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_secrets_sbom.py +0 -0
  267. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_self_state.py +0 -0
  268. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_source_manager.py +0 -0
  269. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_ssa.py +0 -0
  270. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_ssrf_destinations.py +0 -0
  271. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_ssrf_targets.py +0 -0
  272. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_summary_sanitizers.py +0 -0
  273. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_suppressions.py +0 -0
  274. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_symbol_resolution.py +0 -0
  275. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_symbols.py +0 -0
  276. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_syntactic_rules.py +0 -0
  277. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_syntax_control_flow.py +0 -0
  278. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_syntax_coverage.py +0 -0
  279. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_syntax_expressions.py +0 -0
  280. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_taint.py +0 -0
  281. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_template_escaping.py +0 -0
  282. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_tornado_bottle_models.py +0 -0
  283. {coretrace_python_analyzer-0.14.0 → coretrace_python_analyzer-0.15.0}/tests/test_vex.py +0 -0
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.0 (2026-09-26)
4
+
5
+ ### Precision
6
+
7
+ - `open-redirect` judges a flow on the destination a browser resolves from the target's constant text, with its own criteria rather than SSRF's. A reference on the current site keeps the browser there, so the flow is refuted. Such a reference is `/` followed by a character that is not a slash, a backslash, a space or a control character, as in `"/profile/" + id`; or `?`, `#`, or a relative path whose first segment holds no `:`. A fixed absolute host makes the flow a hotspot, since the input chooses the path, where that host may redirect again. Everything else stays a vulnerability, including `"/" + next`: `//evil.com` is another host. The URL facts both rules read are one shared proof, `coretrace_python.taint.urls`, and `ssrf` now reads it too, with the same verdicts. The regression corpus is unchanged: its three `open-redirect` findings redirect to values built without constant text (#168).
8
+
9
+ ### Advisories
10
+
11
+ - An advisory entry point may declare a `host` condition: the attacker must choose the host of the URL passed as the named argument. It is decided from the URL's constant text, the proof `ssrf` and `open-redirect` read. A fixed host with redirects disabled in the call leaves the call reachable, not exploitable. A fixed host with redirects followed keeps it exploitable, because a redirect may lead to a host the attacker controls, and the condition pending review says so. Input in the query does not choose the host. Anything else stays pending review, and so do the environment conditions. A `host` condition must name its argument and carry nothing else (#170).
12
+
3
13
  ## 0.14.0 (2026-09-26)
4
14
 
5
15
  ### Precision
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: coretrace-python-analyzer
3
- Version: 0.14.0
3
+ Version: 0.15.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
@@ -117,7 +117,9 @@ that must not leave the application through a log, a third party or plain storag
117
117
 
118
118
  A rule may know more about its own sinks than the refutation does: `judge(ctx, function,
119
119
  flow, verdict)` returns the rule's verdict on a flow, given the refutation's, and by
120
- default returns it unchanged. `ssrf` reads there how the URL is built. The refutation's
120
+ default returns it unchanged. `ssrf` and `open-redirect` read there what the URL's
121
+ constant text proves, a proof they share (`coretrace_python.taint.urls`) and each
122
+ judges by its own criteria. The refutation's
121
123
  verdict, which other consumers of the flow such as the advisory correlation read, does
122
124
  not change; a rule reading more analyses declares them in its `requires`.
123
125
 
@@ -117,7 +117,7 @@ variable is not treated that way yet.
117
117
  | `ssrf` | The URL of an HTTP client request (Requests, httpx, `urllib`), by position or as `url=`; the body, headers and query parameters are not the destination. How `+` and f-strings build the URL can prove part of its destination. When constant text fixes `scheme://host` and ends the authority with `/`, `?` or `#` before any input (`"https://api.example.com/users/" + id`, or a module-level string bound once and never rebound), the flow is a hotspot: the input still chooses the path, which may reach a sensitive resource, and a redirect may lead elsewhere. With the path fixed too, the input reaching the query or fragment only, and the call disabling redirects (`allow_redirects=False`, `follow_redirects=False`), the destination is fixed and the flow is refuted. Anything unproven stays a vulnerability: a base of unknown value (imported, a call result), a constant leaving the authority open (`"https:/"`, no `/` after the host, a variable port), `urljoin`, `%`, `format`, a URL built in a helper function. |
118
118
  | `xss` | An HTTP response body built without escaping. A Django template rendered by `render_to_string` escapes, where the analyzer can establish it (below). |
119
119
  | `insecure-deserialization` | `pickle`, `marshal`, `dill`, `jsonpickle`, `shelve`, and the YAML loaders (`yaml.load`, `load_all`, `full_load`, `unsafe_load` and their several-document versions); `yaml.load` and `load_all` given a safe loader (`SafeLoader`, `BaseLoader`) are not. |
120
- | `open-redirect` | An HTTP redirect target. |
120
+ | `open-redirect` | An HTTP redirect target, judged on the destination a browser resolves from the target's constant text, as `+` and f-strings build it. A reference on the current site keeps the browser there, where any further redirect is the project's own code, analysed on its own, so the flow is refuted. Such a reference is `/` followed by a character that is not a slash, a backslash, a space or a control character (`"/profile/" + id`), `?`, `#`, or a relative path whose first segment ends in the text without a `:`. A fixed absolute host (`"https://example.com/" + path`) sends the browser there, but the input chooses the path, where that host may redirect again: a hotspot. Anything else stays a vulnerability. That covers `"/" + next`, since `//evil.com` is another host, a host left open, a base of unknown value and a target built in a helper function. |
121
121
  | `plaintext-credential-storage` | A parameter named like a password stored in a database without hashing. Medium confidence, since a name is a hint. |
122
122
  | `exploitable-vulnerability` | An API affected by an advisory of a vulnerable requirement, see Dependencies. |
123
123
 
@@ -241,7 +241,7 @@ the directory of the checked file; a path outside it is printed as it is.
241
241
  ```json
242
242
  {
243
243
  "schema_version": 1,
244
- "tool": {"name": "coretrace-python-analyzer", "version": "0.14.0"},
244
+ "tool": {"name": "coretrace-python-analyzer", "version": "0.15.0"},
245
245
  "root": "/home/me/project",
246
246
  "findings": [
247
247
  {
@@ -273,7 +273,7 @@ bytes. A single-file check lists no components.
273
273
 
274
274
  ```json
275
275
  "tool": {
276
- "name": "coretrace-python-analyzer", "version": "0.14.0",
276
+ "name": "coretrace-python-analyzer", "version": "0.15.0",
277
277
  "components": [
278
278
  {"kind": "plugin", "name": "curated-advisories", "version": "2026.09.25", "digest": "sha256:…"},
279
279
  {"kind": "advisories", "name": "advisories.json", "digest": "sha256:…"}
@@ -386,6 +386,23 @@ a call passing a variable, or unpacking `*args` or `**kwargs`, leaves the condit
386
386
  pending review. A `semantic` condition — the document's tags, the platform, the
387
387
  template's origin — cannot be decided and is always pending review, never dropped.
388
388
 
389
+ A `host` condition says the attacker must choose the host of the URL passed as
390
+ `argument` (or at `position`), as credentials leaking to the host of a crafted URL
391
+ need: `{"kind": "host", "text": "…", "argument": "url", "position": 0}`. It is
392
+ decided from the URL's constant text, as `ssrf` and `open-redirect` read it, when the
393
+ attacker's input is passed in that argument. The outcome has three cases:
394
+
395
+ - Constant text fixes the host (`"https://api.example.com/" + path`) and the call
396
+ disables redirects (`allow_redirects=False`, `follow_redirects=False`): the call is
397
+ reachable, not exploitable.
398
+ - Constant text fixes the host but redirects may be followed: the call stays
399
+ exploitable. The fixed initial host does not clear the chain, since a redirect may
400
+ lead to a host the attacker controls, and the condition pending review says so.
401
+ Input in the query does not choose the host.
402
+ - Otherwise the condition stays pending review.
403
+
404
+ The other conditions, the environment included, stay pending as before.
405
+
389
406
  Every dependency finding records the highest level of evidence established in its
390
407
  `level` metadata: `declared` (the requirement allows a vulnerable version), `imported`
391
408
  (a module of the package is imported somewhere), `reachable` (an entry point or affected
@@ -436,7 +453,7 @@ project's behalf would go unseen. Without a lock file, a package is never
436
453
  `not_affected`. The document's `@id` derives from its statements, so two runs with the
437
454
  same result share it; its `author` is `Unknown Author`, as OpenVEX tools write when they
438
455
  cannot know it. Its `tooling` names the analyzer and each component with its digest
439
- (`coretrace-python-analyzer 0.14.0; curated-advisories 2026.09.25 (sha256:…)`), so the
456
+ (`coretrace-python-analyzer 0.15.0; curated-advisories 2026.09.25 (sha256:…)`), so the
440
457
  `@id` changes with the advisory data a statement was decided with.
441
458
 
442
459
  ## Large projects
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "coretrace-python-analyzer"
7
- version = "0.14.0"
7
+ version = "0.15.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.14.0"
3
+ __version__ = "0.15.0"
4
4
 
@@ -0,0 +1,47 @@
1
+ """Open redirect: attacker-controlled input reaching a open redirect sink.
2
+
3
+ The rule draws its verdict from what the target's constant text proves (``taint.urls``),
4
+ as a browser resolves it. A reference on the current site (``/profile/``, ``?page=``,
5
+ ``profile/``) keeps the browser on the site, where any further redirect is the project's
6
+ own code, analysed on its own: refuted. A fixed absolute host sends the browser there,
7
+ but the input chooses the path, where that host may redirect again: a hotspot. Anything
8
+ unproven, ``"/" + next`` included, stays a vulnerability.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import ClassVar
14
+
15
+ from coretrace_python.abstract.strings import ModuleStringsAnalysis
16
+ from coretrace_python.analysis import AnyAnalysis
17
+ from coretrace_python.findings import Severity
18
+ from coretrace_python.findings.refutation import Status, Verdict
19
+ from coretrace_python.hir import nodes
20
+ from coretrace_python.ir.ssa import SSAAnalysis
21
+ from coretrace_python.plugins import PluginContext, TaintDetector
22
+ from coretrace_python.taint import TaintFlow, TaintKind
23
+ from coretrace_python.taint.urls import flow_url
24
+
25
+
26
+ class OpenRedirectPlugin(TaintDetector):
27
+ name: ClassVar[str] = "open-redirect"
28
+ rule_id: ClassVar[str] = "open-redirect"
29
+ kind: ClassVar[TaintKind] = TaintKind.REDIRECT
30
+ severity: ClassVar[Severity] = Severity.MEDIUM
31
+ title: ClassVar[str] = "Open redirect"
32
+ requires: ClassVar[frozenset[AnyAnalysis]] = TaintDetector.requires | {SSAAnalysis, ModuleStringsAnalysis}
33
+
34
+ def judge(self, ctx: PluginContext, function: nodes.Function, flow: TaintFlow, verdict: Verdict) -> Verdict:
35
+ ssa = ctx.get(SSAAnalysis, function)
36
+ defs = {i.result: i for block in ssa.blocks for i in block.instructions if i.result is not None}
37
+ target = flow_url(flow, defs, ctx.get(ModuleStringsAnalysis))
38
+ if target.relative:
39
+ return Verdict(flow, Status.REFUTED, f"the browser stays on the site: the target starts with {target.text!r}")
40
+ if target.origin is not None and verdict.status is Status.VULNERABILITY:
41
+ return Verdict(
42
+ flow,
43
+ Status.HOTSPOT,
44
+ f"the browser goes to the host fixed by {target.origin!r}, but the input chooses the path, "
45
+ "where that host may redirect again",
46
+ )
47
+ return verdict
@@ -1,7 +1,7 @@
1
1
  name = "open-redirect"
2
2
  version = "1.0.0"
3
3
  plugin_api = ">=1,<2"
4
- requires = ["taint.flows", "findings.refutation"]
4
+ requires = ["taint.flows", "findings.refutation", "ir.ssa", "abstract.module_strings"]
5
5
  provides = ["vulnerability.open-redirect"]
6
6
 
7
7
  [entrypoint]
@@ -0,0 +1,49 @@
1
+ """Server-side request forgery: attacker-controlled input reaching a SSRF sink.
2
+
3
+ The rule draws its verdict from what the URL's constant text proves (``taint.urls``).
4
+ A fixed host keeps the input from choosing it, but the input still chooses the path,
5
+ which may reach a sensitive resource of that host, and a redirect may lead elsewhere: a
6
+ hotspot. With the path fixed too, the input reaching the query or fragment only, and
7
+ redirects disabled in the call, the destination is fixed: refuted. Anything unproven,
8
+ a base of unknown value or a URL built in a helper function, stays a vulnerability.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import ClassVar
14
+
15
+ from coretrace_python.abstract.strings import ModuleStringsAnalysis
16
+ from coretrace_python.analysis import AnyAnalysis
17
+ from coretrace_python.findings import Severity
18
+ from coretrace_python.findings.refutation import Status, Verdict
19
+ from coretrace_python.hir import nodes
20
+ from coretrace_python.ir.ssa import SSAAnalysis
21
+ from coretrace_python.plugins import PluginContext, TaintDetector
22
+ from coretrace_python.taint import TaintFlow, TaintKind
23
+ from coretrace_python.taint.urls import flow_url, redirects_disabled
24
+
25
+
26
+ class SsrfPlugin(TaintDetector):
27
+ name: ClassVar[str] = "ssrf"
28
+ rule_id: ClassVar[str] = "ssrf"
29
+ kind: ClassVar[TaintKind] = TaintKind.SSRF
30
+ severity: ClassVar[Severity] = Severity.HIGH
31
+ title: ClassVar[str] = "Server-side request forgery"
32
+ requires: ClassVar[frozenset[AnyAnalysis]] = TaintDetector.requires | {SSAAnalysis, ModuleStringsAnalysis}
33
+
34
+ def judge(self, ctx: PluginContext, function: nodes.Function, flow: TaintFlow, verdict: Verdict) -> Verdict:
35
+ ssa = ctx.get(SSAAnalysis, function)
36
+ defs = {i.result: i for block in ssa.blocks for i in block.instructions if i.result is not None}
37
+ url = flow_url(flow, defs, ctx.get(ModuleStringsAnalysis))
38
+ if url.origin is None:
39
+ return verdict
40
+ no_redirects = redirects_disabled(flow.sink_arguments)
41
+ if url.path_fixed:
42
+ if no_redirects:
43
+ return Verdict(flow, Status.REFUTED, f"the destination is fixed by {url.text!r} and redirects are disabled")
44
+ left = "a redirect may lead elsewhere"
45
+ else:
46
+ left = "the input chooses the path" + ("" if no_redirects else ", and a redirect may lead elsewhere")
47
+ if verdict.status is not Status.VULNERABILITY:
48
+ return verdict
49
+ return Verdict(flow, Status.HOTSPOT, f"the host is fixed by {url.origin!r}, but {left}")
@@ -251,7 +251,7 @@ def _condition(entry: Mapping[str, Any]) -> Condition:
251
251
  default = entry.get("default", False)
252
252
  if not isinstance(default, bool):
253
253
  raise TypeError(f"condition default must be true or false, got {default!r}")
254
- return Condition(
254
+ condition = Condition(
255
255
  str(entry["kind"]),
256
256
  str(entry["text"]),
257
257
  None if entry.get("argument") is None else str(entry["argument"]),
@@ -259,3 +259,8 @@ def _condition(entry: Mapping[str, Any]) -> Condition:
259
259
  position,
260
260
  default,
261
261
  )
262
+ if condition.kind == "host" and (
263
+ (condition.argument is None and condition.position is None) or condition.values or condition.default
264
+ ):
265
+ raise ValueError("a host condition names the URL argument, by keyword or position, and nothing else")
266
+ return condition
@@ -10,7 +10,7 @@ are correlated here into one high-confidence ``exploitable-vulnerability`` findi
10
10
  from __future__ import annotations
11
11
 
12
12
  from collections.abc import Iterable, Mapping
13
- from dataclasses import dataclass
13
+ from dataclasses import dataclass, replace
14
14
 
15
15
  from coretrace_python.dependency.graph import (
16
16
  DIRECT,
@@ -24,6 +24,7 @@ from coretrace_python.findings.refutation import Status, Verdict, Verdicts
24
24
  from coretrace_python.interprocedural import Arguments, CallSite, ExternalSymbol
25
25
  from coretrace_python.semantic.symbols import SymbolId
26
26
  from coretrace_python.taint import Sink, TaintFlow, TaintKind
27
+ from coretrace_python.taint.urls import UrlProof, redirects_disabled
27
28
 
28
29
  Affected = Mapping[SymbolId, tuple[Advisory, ...]]
29
30
 
@@ -61,14 +62,33 @@ class ConditionCheck:
61
62
  passed: str | None = None
62
63
 
63
64
 
64
- def check_conditions(entry: AdvisoryEntryPoint | None, arguments: Arguments | None) -> ConditionCheck:
65
- """Decide ``entry``'s conditions against what the call's ``arguments`` denote."""
65
+ def check_conditions(
66
+ entry: AdvisoryEntryPoint | None,
67
+ arguments: Arguments | None,
68
+ url: UrlProof | None = None,
69
+ passed_as: frozenset[tuple[int | None, str | None]] = frozenset(),
70
+ ) -> ConditionCheck:
71
+ """Decide ``entry``'s conditions against what the call's ``arguments`` denote and, for
72
+ a ``host`` condition, what ``url`` proves of the value the attacker's input is passed
73
+ in (``passed_as``). A host the URL's constant text fixes rules the call out when the
74
+ call disables redirects; otherwise a redirect may still lead to a host the attacker
75
+ controls, and the condition stays pending with that uncertainty."""
66
76
 
67
77
  if entry is None:
68
78
  return ConditionCheck()
69
79
  met: list[Condition] = []
70
80
  pending: list[Condition] = []
71
81
  for condition in entry.conditions:
82
+ if condition.kind == "host":
83
+ origin = url.origin if url is not None and _passed_in(condition, passed_as) else None
84
+ if origin is None:
85
+ pending.append(condition)
86
+ elif redirects_disabled(arguments):
87
+ return ConditionCheck(tuple(met), tuple(pending), condition, repr(origin))
88
+ else:
89
+ text = f"the host is fixed by {origin!r}, but a redirect may lead to a host the attacker controls"
90
+ pending.append(replace(condition, text=text))
91
+ continue
72
92
  given = arguments.given(condition.argument, condition.position) if condition.checkable and arguments is not None else None
73
93
  if given is None:
74
94
  pending.append(condition)
@@ -84,6 +104,16 @@ def check_conditions(entry: AdvisoryEntryPoint | None, arguments: Arguments | No
84
104
  return ConditionCheck(tuple(met), tuple(pending))
85
105
 
86
106
 
107
+ def _passed_in(condition: Condition, passed_as: frozenset[tuple[int | None, str | None]]) -> bool:
108
+ """Whether a value passed as ``passed_as`` is the argument ``condition`` names."""
109
+
110
+ return any(
111
+ (keyword is not None and keyword == condition.argument)
112
+ or (keyword is None and position is not None and position == condition.position)
113
+ for position, keyword in passed_as
114
+ )
115
+
116
+
87
117
  def ruled_out(module: str, site: CallSite, check: ConditionCheck) -> str:
88
118
  """Which call a contradicted condition rules out, and why: ``app:12
89
119
  python.yaml.load(Loader=python.yaml.SafeLoader)``."""
@@ -120,9 +150,10 @@ def correlate(
120
150
  flows: Iterable[TaintFlow],
121
151
  verdicts: Verdicts | None,
122
152
  affected: Affected,
153
+ urls: Mapping[TaintFlow, UrlProof] | None = None,
123
154
  ) -> tuple[Finding, ...]:
124
155
  """Exploitable-vulnerability findings for the non-refuted ADVISORY flows of a function,
125
- one per advisory the sink is affected by."""
156
+ one per advisory the sink is affected by; ``urls`` holds what each flow's URL proves."""
126
157
 
127
158
  findings: list[Finding] = []
128
159
  for flow in flows:
@@ -136,7 +167,7 @@ def correlate(
136
167
  entry = advisory.entry_point(flow.sink.symbol)
137
168
  if entry is not None and not any(entry.exploitable_through(p, k) for p, k in flow.passed_as):
138
169
  continue
139
- check = check_conditions(entry, flow.sink_arguments)
170
+ check = check_conditions(entry, flow.sink_arguments, (urls or {}).get(flow), flow.passed_as)
140
171
  if check.contradicted is None:
141
172
  findings.append(_exploitable(function, flow, advisory, verdict, hotspot, check))
142
173
  return tuple(findings)
@@ -151,8 +151,10 @@ class Condition:
151
151
  ``argument`` condition names the argument by keyword, and by ``position`` when it may
152
152
  be passed positionally, and lists the ``values`` that satisfy it — symbols
153
153
  (``python.yaml.FullLoader``) or constants as Python writes them (``True``);
154
- ``default`` says an absent argument means a vulnerable value. A ``semantic``
155
- condition cannot be checked and is reported as pending review."""
154
+ ``default`` says an absent argument means a vulnerable value. A ``host`` condition
155
+ says the attacker must choose the host of the URL passed as ``argument`` (or at
156
+ ``position``); the engine decides it from what the URL's constant text proves. A
157
+ ``semantic`` condition cannot be checked and is reported as pending review."""
156
158
 
157
159
  kind: str
158
160
  text: str
@@ -126,6 +126,7 @@ from coretrace_python.taint import (
126
126
  escaped_templates,
127
127
  registered_routes,
128
128
  )
129
+ from coretrace_python.taint.urls import flow_url
129
130
 
130
131
  TOOL_NAME = "coretrace-python-analyzer"
131
132
 
@@ -770,14 +771,14 @@ def _analyse_module(
770
771
  graph = manager.get(CallGraphAnalysis)
771
772
  correlated: list[Finding] = []
772
773
  if affected:
774
+ strings = manager.get(ModuleStringsAnalysis)
773
775
  for function in supported:
776
+ flows = manager.get(TaintAnalysis, function).flows
777
+ ssa = manager.get(SSAAnalysis, function)
778
+ defs = {i.result: i for block in ssa.blocks for i in block.instructions if i.result is not None}
779
+ urls = {flow: flow_url(flow, defs, strings) for flow in flows if flow.kinds & TaintKind.ADVISORY}
774
780
  correlated.extend(
775
- correlate(
776
- graph.name_of(function),
777
- manager.get(TaintAnalysis, function).flows,
778
- manager.get(RefutationAnalysis, function),
779
- affected,
780
- )
781
+ correlate(graph.name_of(function), flows, manager.get(RefutationAnalysis, function), affected, urls)
781
782
  )
782
783
  return CachedModule(
783
784
  manager.get(EntryPointAnalysis),
@@ -0,0 +1,70 @@
1
+ """What the constant text a URL starts with proves about its destination, and whether a
2
+ call follows redirects: one proof that the rules reading URLs share, each drawing its
3
+ own verdict from it. The text is what ``+`` and f-strings build before any other value
4
+ (``abstract.strings``); the proof says nothing about the rest of the URL."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from collections.abc import Mapping
10
+ from dataclasses import dataclass
11
+
12
+ from coretrace_python.abstract.strings import leading_text
13
+ from coretrace_python.interprocedural import Arguments
14
+ from coretrace_python.ir.model import Instruction, Value
15
+ from coretrace_python.taint.engine import TaintFlow
16
+
17
+ # ``scheme://`` and an authority ended by ``/``, ``?`` or ``#``. A backslash, which some
18
+ # URL parsers take for a slash, does not end it here.
19
+ _ORIGIN = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://[^/?#\\]+[/?#]")
20
+ # The same with the path fixed too: what follows is query or fragment.
21
+ _PATH = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://[^/?#\\]+(?:/[^?#]*)?[?#]")
22
+ # A reference a browser resolves on the current site: ``/`` then a character that is not
23
+ # a slash, a backslash, a space or a control character (``//`` and ``/\`` name another
24
+ # host, and a browser drops tabs and newlines), a query or a fragment, or a relative path
25
+ # whose first segment ends in the text without a ``:`` that would make it a scheme.
26
+ _RELATIVE = re.compile(r"/[^/\\\x00-\x20\x7f]|[?#]|[^/\\?#:\x00-\x20\x7f][^/\\?#:]*[/?#]")
27
+ # A client honours the one of these keywords it knows and rejects the other with a
28
+ # ``TypeError``: either way, the call follows no redirect.
29
+ _NO_REDIRECTS = ("allow_redirects", "follow_redirects")
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class UrlProof:
34
+ """What the constant ``text`` a URL starts with proves: the ``origin`` it fixes
35
+ (``scheme://authority`` and the character ending it), whether it fixes the path
36
+ too (``path_fixed``), and whether it starts a reference a browser resolves on the
37
+ current site (``relative``)."""
38
+
39
+ text: str
40
+ origin: str | None = None
41
+ path_fixed: bool = False
42
+ relative: bool = False
43
+
44
+
45
+ def url_proof(text: str) -> UrlProof:
46
+ origin = _ORIGIN.match(text)
47
+ return UrlProof(
48
+ text,
49
+ origin.group(0) if origin is not None else None,
50
+ _PATH.match(text) is not None,
51
+ _RELATIVE.match(text) is not None,
52
+ )
53
+
54
+
55
+ def flow_url(flow: TaintFlow, defs: Mapping[Value, Instruction], strings: Mapping[str, str]) -> UrlProof:
56
+ """The proof for the value ``flow`` passes to its sink, ``defs`` defining the values
57
+ of the function making the call. A flow reaching its sink in a callee proves nothing:
58
+ the value is built there."""
59
+
60
+ if flow.through is not None:
61
+ return UrlProof("")
62
+ text, _ = leading_text(flow.argument, defs, strings)
63
+ return url_proof(text)
64
+
65
+
66
+ def redirects_disabled(arguments: Arguments | None) -> bool:
67
+ """Whether the call disables redirects explicitly (``allow_redirects=False`` or
68
+ ``follow_redirects=False``); absent, a variable or unpacked options do not."""
69
+
70
+ return arguments is not None and any(arguments.given(name) == (True, "False") for name in _NO_REDIRECTS)
@@ -0,0 +1,145 @@
1
+ """An advisory entry point may declare that exploiting it needs the attacker to choose the
2
+ host of a URL: a ``host`` condition naming the argument. The engine decides it from what
3
+ the URL's constant text proves (``taint.urls``), the proof ``ssrf`` and ``open-redirect``
4
+ read, with the advisory's own criteria:
5
+
6
+ - constant text fixing the host, and the call disabling redirects: the attacker cannot
7
+ choose the host, so the call is reachable, not exploitable;
8
+ - constant text fixing the host, redirects followed: the initial host does not clear the
9
+ chain, since a redirect may lead to a host the attacker controls. The call stays
10
+ exploitable, and the condition pending review says so. Input in the query does not
11
+ choose the host;
12
+ - anything else: the condition stays pending review, undecided.
13
+
14
+ The other conditions, those on the environment included, stay pending as before.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ from pathlib import Path
21
+
22
+ import pytest
23
+
24
+ from coretrace_python import engine
25
+ from coretrace_python.dependency import (
26
+ Advisory,
27
+ AdvisoryEntryPoint,
28
+ AdvisoryFileError,
29
+ AttackerArgument,
30
+ Condition,
31
+ dump_advisories,
32
+ load_advisories,
33
+ )
34
+ from coretrace_python.findings import Finding, Severity
35
+ from coretrace_python.semantic.symbols import SymbolId
36
+
37
+ HOST = Condition("host", "the attacker chooses the host of the URL", argument="url", position=0)
38
+ NETRC = Condition("semantic", "a .netrc file holds credentials for the host")
39
+ FETCH = Advisory(
40
+ "CVE-2099-0901",
41
+ "vulnlib",
42
+ "<1.1",
43
+ "fetch sends the credentials of one host to another",
44
+ Severity.HIGH,
45
+ entry_points=(
46
+ AdvisoryEntryPoint(
47
+ SymbolId("python.vulnlib.fetch"),
48
+ "commit abc1234",
49
+ (HOST, NETRC),
50
+ attacker_arguments=(AttackerArgument("url", 0),),
51
+ ),
52
+ ),
53
+ modules=("vulnlib",),
54
+ )
55
+ FIXED = "the host is fixed by 'https://api.example.com/', but a redirect may lead to a host the attacker controls"
56
+
57
+
58
+ def analyse(root: Path, call: str) -> tuple[Finding, ...]:
59
+ (root / "requirements.txt").write_text("vulnlib==1.0\n", encoding="utf-8")
60
+ (root / "advisories.json").write_text(dump_advisories((FETCH,)), encoding="utf-8")
61
+ (root / "app.py").write_text(
62
+ f"import vulnlib\n\ndef run():\n data = input()\n return {call}\n", encoding="utf-8"
63
+ )
64
+ return engine.analyze_project(root, [engine.BUNDLED_PLUGINS]).findings
65
+
66
+
67
+ def exploitable(root: Path, call: str) -> list[str]:
68
+ """The conditions pending review of each exploitable finding."""
69
+
70
+ return [
71
+ f.metadata.get("conditions_pending_review", "")
72
+ for f in analyse(root, call)
73
+ if f.rule_id == "exploitable-vulnerability"
74
+ ]
75
+
76
+
77
+ @pytest.mark.parametrize(
78
+ "call",
79
+ [
80
+ "vulnlib.fetch(data)",
81
+ # The host is left open: ``data='@evil.example'`` chooses it.
82
+ "vulnlib.fetch('https://api.example.com' + data, allow_redirects=False)",
83
+ "vulnlib.fetch(BASE + data)",
84
+ ],
85
+ )
86
+ def test_a_host_not_proven_fixed_leaves_the_condition_pending(tmp_path: Path, call: str) -> None:
87
+ assert exploitable(tmp_path, call) == [f"{HOST.text}; {NETRC.text}"]
88
+
89
+
90
+ @pytest.mark.parametrize(
91
+ "call",
92
+ [
93
+ "vulnlib.fetch('https://api.example.com/users/' + data)",
94
+ # Input in the query does not choose the host either.
95
+ "vulnlib.fetch(f'https://api.example.com/search?q={data}')",
96
+ ],
97
+ )
98
+ def test_a_fixed_host_with_redirects_followed_keeps_the_chain_visible(tmp_path: Path, call: str) -> None:
99
+ assert exploitable(tmp_path, call) == [f"{FIXED}; {NETRC.text}"]
100
+
101
+
102
+ @pytest.mark.parametrize(
103
+ "call",
104
+ [
105
+ "vulnlib.fetch('https://api.example.com/users/' + data, allow_redirects=False)",
106
+ "vulnlib.fetch(url='https://api.example.com/users/' + data, follow_redirects=False)",
107
+ ],
108
+ )
109
+ def test_a_fixed_host_without_redirects_leaves_the_call_reachable_only(tmp_path: Path, call: str) -> None:
110
+ findings = analyse(tmp_path, call)
111
+
112
+ assert [f for f in findings if f.rule_id == "exploitable-vulnerability"] == []
113
+ assert [f.metadata["advisory"] for f in findings if f.rule_id == "reachable-vulnerability"] == [FETCH.id]
114
+
115
+
116
+ def test_host_conditions_round_trip_through_advisory_files(tmp_path: Path) -> None:
117
+ path = tmp_path / "advisories.json"
118
+ path.write_text(dump_advisories((FETCH,)), encoding="utf-8")
119
+
120
+ assert load_advisories(path) == (FETCH,)
121
+ (entry,) = json.loads(path.read_text())["advisories"][0]["entry_points"]
122
+ assert entry["conditions"][0] == {
123
+ "kind": "host",
124
+ "text": "the attacker chooses the host of the URL",
125
+ "argument": "url",
126
+ "position": 0,
127
+ }
128
+
129
+
130
+ @pytest.mark.parametrize(
131
+ "condition",
132
+ [
133
+ {"kind": "host", "text": "the attacker chooses the host"},
134
+ {"kind": "host", "text": "the attacker chooses the host", "argument": "url", "values": ["x"]},
135
+ {"kind": "host", "text": "the attacker chooses the host", "argument": "url", "default": True},
136
+ ],
137
+ )
138
+ def test_a_host_condition_names_its_argument_and_nothing_else(tmp_path: Path, condition: dict[str, object]) -> None:
139
+ document = json.loads(dump_advisories((FETCH,)))
140
+ document["advisories"][0]["entry_points"][0]["conditions"] = [condition]
141
+ path = tmp_path / "advisories.json"
142
+ path.write_text(json.dumps(document), encoding="utf-8")
143
+
144
+ with pytest.raises(AdvisoryFileError):
145
+ load_advisories(path)