classifyre-cli 0.4.74__tar.gz → 0.4.76__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 (266) hide show
  1. classifyre_cli-0.4.76/.turbo/turbo-build.log +3 -0
  2. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/PKG-INFO +1 -1
  3. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/package.json +1 -1
  4. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/pyproject.toml +1 -1
  5. classifyre_cli-0.4.76/src/detectors/engine_version.py +53 -0
  6. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/main.py +108 -7
  7. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/models/generated_input.py +63 -0
  8. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/models/generated_single_asset_scan_results.py +52 -0
  9. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/outputs/rest.py +35 -3
  10. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/pipeline/detector_pipeline.py +39 -4
  11. classifyre_cli-0.4.76/src/pipeline/scan_cache.py +545 -0
  12. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/atlassian_common.py +3 -2
  13. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/base.py +24 -0
  14. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/email/source.py +8 -2
  15. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/google_workspace/source.py +9 -2
  16. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/local_folder/source.py +6 -0
  17. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/microsoft_365/source.py +7 -2
  18. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/notion/client.py +3 -1
  19. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/object_storage/base.py +34 -6
  20. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/recipe_normalizer.py +11 -0
  21. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/utils/file_parser.py +52 -10
  22. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_s3_compatible_storage_source.py +58 -0
  23. classifyre_cli-0.4.76/tests/test_scan_cache.py +600 -0
  24. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_source_strategy_conformance.py +62 -0
  25. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/utils/test_file_parser.py +73 -0
  26. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/uv.lock +28 -25
  27. classifyre_cli-0.4.74/.turbo/turbo-build.log +0 -3
  28. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/.gitignore +0 -0
  29. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/.python-version +0 -0
  30. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/README.md +0 -0
  31. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/main.py +0 -0
  32. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/scripts/generate_models.py +0 -0
  33. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/__init__.py +0 -0
  34. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/config.py +0 -0
  35. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/__init__.py +0 -0
  36. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/base.py +0 -0
  37. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/broken_links/__init__.py +0 -0
  38. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/broken_links/detector.py +0 -0
  39. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/config.py +0 -0
  40. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/content/__init__.py +0 -0
  41. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/custom/__init__.py +0 -0
  42. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/custom/detector.py +0 -0
  43. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/custom/extractor.py +0 -0
  44. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/custom/runners/__init__.py +0 -0
  45. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/custom/runners/_base.py +0 -0
  46. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/custom/runners/_factory.py +0 -0
  47. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/custom/runners/_gliner2.py +0 -0
  48. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/custom/runners/_image_classification.py +0 -0
  49. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/custom/runners/_llm.py +0 -0
  50. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/custom/runners/_object_detection.py +0 -0
  51. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/custom/runners/_regex.py +0 -0
  52. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/custom/runners/_text_classification.py +0 -0
  53. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/custom/trainer.py +0 -0
  54. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/dependencies.py +0 -0
  55. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/pii/__init__.py +0 -0
  56. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/pii/detector.py +0 -0
  57. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/secrets/__init__.py +0 -0
  58. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/secrets/detector.py +0 -0
  59. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/threat/__init__.py +0 -0
  60. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/threat/code_security_detector.py +0 -0
  61. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/detectors/threat/yara_detector.py +0 -0
  62. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/file_evaluation/__init__.py +0 -0
  63. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/file_evaluation/runner.py +0 -0
  64. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/models/generated_detectors.py +0 -0
  65. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/outputs/__init__.py +0 -0
  66. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/outputs/base.py +0 -0
  67. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/outputs/console.py +0 -0
  68. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/outputs/factory.py +0 -0
  69. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/outputs/file.py +0 -0
  70. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/pipeline/__init__.py +0 -0
  71. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/pipeline/content_provider.py +0 -0
  72. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/pipeline/parsed_content_provider.py +0 -0
  73. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/pipeline/text_artifact.py +0 -0
  74. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/pipeline/worker_pool.py +0 -0
  75. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/__init__.py +0 -0
  76. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/asset_metadata.py +0 -0
  77. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/azure_blob_storage/__init__.py +0 -0
  78. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/azure_blob_storage/source.py +0 -0
  79. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/confluence/__init__.py +0 -0
  80. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/confluence/source.py +0 -0
  81. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/databricks/__init__.py +0 -0
  82. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/databricks/source.py +0 -0
  83. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/delta_lake/__init__.py +0 -0
  84. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/delta_lake/source.py +0 -0
  85. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/dependencies.py +0 -0
  86. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/dropbox/__init__.py +0 -0
  87. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/dropbox/auth.py +0 -0
  88. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/dropbox/source.py +0 -0
  89. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/elasticsearch/__init__.py +0 -0
  90. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/elasticsearch/source.py +0 -0
  91. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/email/__init__.py +0 -0
  92. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/google_cloud_storage/__init__.py +0 -0
  93. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/google_cloud_storage/source.py +0 -0
  94. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/google_workspace/__init__.py +0 -0
  95. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/hive/__init__.py +0 -0
  96. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/hive/source.py +0 -0
  97. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/iceberg/__init__.py +0 -0
  98. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/iceberg/source.py +0 -0
  99. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/jira/__init__.py +0 -0
  100. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/jira/source.py +0 -0
  101. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/kafka/__init__.py +0 -0
  102. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/kafka/rest.py +0 -0
  103. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/kafka/source.py +0 -0
  104. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/lakehouse_base.py +0 -0
  105. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/local_folder/__init__.py +0 -0
  106. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/meilisearch/__init__.py +0 -0
  107. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/meilisearch/source.py +0 -0
  108. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/microsoft_365/__init__.py +0 -0
  109. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/mongodb/__init__.py +0 -0
  110. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/mongodb/source.py +0 -0
  111. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/mssql/__init__.py +0 -0
  112. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/mssql/source.py +0 -0
  113. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/mysql/__init__.py +0 -0
  114. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/mysql/source.py +0 -0
  115. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/neo4j/__init__.py +0 -0
  116. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/neo4j/source.py +0 -0
  117. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/notion/__init__.py +0 -0
  118. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/notion/source.py +0 -0
  119. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/opensearch/__init__.py +0 -0
  120. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/opensearch/source.py +0 -0
  121. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/oracle/__init__.py +0 -0
  122. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/oracle/source.py +0 -0
  123. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/postgresql/__init__.py +0 -0
  124. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/postgresql/source.py +0 -0
  125. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/powerbi/__init__.py +0 -0
  126. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/powerbi/source.py +0 -0
  127. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/s3_client.py +0 -0
  128. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/s3_compatible_storage/README.md +0 -0
  129. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/s3_compatible_storage/__init__.py +0 -0
  130. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/s3_compatible_storage/source.py +0 -0
  131. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/sandbox/__init__.py +0 -0
  132. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/sandbox/source.py +0 -0
  133. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/search_engine_base.py +0 -0
  134. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/servicedesk/__init__.py +0 -0
  135. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/servicedesk/source.py +0 -0
  136. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/slack/__init__.py +0 -0
  137. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/slack/source.py +0 -0
  138. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/snowflake/__init__.py +0 -0
  139. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/snowflake/source.py +0 -0
  140. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/sqlite/__init__.py +0 -0
  141. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/sqlite/source.py +0 -0
  142. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/tableau/__init__.py +0 -0
  143. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/tableau/source.py +0 -0
  144. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/tabular_base.py +0 -0
  145. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/tabular_utils.py +0 -0
  146. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/wordpress/__init__.py +0 -0
  147. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/wordpress/source.py +0 -0
  148. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/youtube/__init__.py +0 -0
  149. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/sources/youtube/source.py +0 -0
  150. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/telemetry.py +0 -0
  151. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/utils/__init__.py +0 -0
  152. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/utils/archive_extraction.py +0 -0
  153. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/utils/content_extraction.py +0 -0
  154. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/utils/dependency_groups.py +0 -0
  155. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/utils/embedded_images.py +0 -0
  156. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/utils/file_metadata.py +0 -0
  157. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/utils/file_to_images.py +0 -0
  158. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/utils/hashing.py +0 -0
  159. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/utils/legacy_office.py +0 -0
  160. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/utils/resources.py +0 -0
  161. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/utils/transcription.py +0 -0
  162. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/utils/uv_sync.py +0 -0
  163. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/utils/validation.py +0 -0
  164. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/src/utils/video_processing.py +0 -0
  165. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/__init__.py +0 -0
  166. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/_lakehouse_fakes.py +0 -0
  167. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/conftest.py +0 -0
  168. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/__init__.py +0 -0
  169. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
  170. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/conftest.py +0 -0
  171. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/content/__init__.py +0 -0
  172. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/custom/__init__.py +0 -0
  173. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/custom/conftest.py +0 -0
  174. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/custom/test_gliner2_classification.py +0 -0
  175. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/custom/test_invoice_extraction.py +0 -0
  176. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/custom/test_llm_runner.py +0 -0
  177. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/custom/test_pipeline_integration.py +0 -0
  178. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/custom/test_regex_runner.py +0 -0
  179. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/custom/test_runner_hardening.py +0 -0
  180. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/custom/test_transformer_runners.py +0 -0
  181. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/pii/__init__.py +0 -0
  182. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/pii/conftest.py +0 -0
  183. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/pii/sample_invoice.pdf +0 -0
  184. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/pii/test_pii_chunking.py +0 -0
  185. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/pii/test_pii_detector.py +0 -0
  186. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
  187. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/pii/test_pii_regional_entities.py +0 -0
  188. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/pii/test_pii_severity.py +0 -0
  189. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/secrets/__init__.py +0 -0
  190. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/secrets/test_secrets_detector.py +0 -0
  191. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
  192. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/test_base_detector.py +0 -0
  193. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
  194. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/test_detector_catalog_commercial.py +0 -0
  195. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/test_detector_pipeline_types.py +0 -0
  196. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/test_detector_schema_examples.py +0 -0
  197. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/test_detector_types.py +0 -0
  198. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/test_phase2_detectors.py +0 -0
  199. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/test_registry.py +0 -0
  200. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/threat/__init__.py +0 -0
  201. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/threat/test_code_security_detector.py +0 -0
  202. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/detectors/threat/test_yara_detector.py +0 -0
  203. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
  204. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/integration/test_wordpress_links_assets.py +0 -0
  205. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/pipeline/test_detector_outcomes.py +0 -0
  206. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/pipeline/test_detector_pipeline.py +0 -0
  207. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/pipeline/test_worker_pool.py +0 -0
  208. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_assets_metadata_catalog.py +0 -0
  209. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_azure_blob_storage_source.py +0 -0
  210. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_base_source_attachment.py +0 -0
  211. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_base_source_sampling.py +0 -0
  212. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_config.py +0 -0
  213. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_confluence_source.py +0 -0
  214. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_custom_extractor.py +0 -0
  215. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_databricks_source.py +0 -0
  216. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_delta_lake_source.py +0 -0
  217. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_dependency_groups.py +0 -0
  218. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_dropbox_auth_command.py +0 -0
  219. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_dropbox_source.py +0 -0
  220. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_elasticsearch_source.py +0 -0
  221. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_email_source.py +0 -0
  222. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_file_evaluation_runner.py +0 -0
  223. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_google_cloud_storage_source.py +0 -0
  224. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_google_workspace_source.py +0 -0
  225. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_hashing.py +0 -0
  226. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_hive_source.py +0 -0
  227. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_iceberg_source.py +0 -0
  228. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_jira_source.py +0 -0
  229. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_kafka_source.py +0 -0
  230. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_local_folder_source.py +0 -0
  231. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_main.py +0 -0
  232. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_meilisearch_source.py +0 -0
  233. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_microsoft_365_source.py +0 -0
  234. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_mongodb_source.py +0 -0
  235. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_mssql_source.py +0 -0
  236. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_mysql_source.py +0 -0
  237. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_neo4j_source.py +0 -0
  238. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_notion_source.py +0 -0
  239. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_opensearch_source.py +0 -0
  240. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_oracle_source.py +0 -0
  241. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_outputs.py +0 -0
  242. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_postgresql_source.py +0 -0
  243. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_powerbi_source.py +0 -0
  244. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_recipe_normalizer.py +0 -0
  245. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_sampling_automatic.py +0 -0
  246. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_sandbox_source.py +0 -0
  247. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_servicedesk_source.py +0 -0
  248. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_slack_source.py +0 -0
  249. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_snowflake_source.py +0 -0
  250. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_source_dependency_groups.py +0 -0
  251. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_sqlite_source.py +0 -0
  252. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_tableau_source.py +0 -0
  253. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_tabular_automatic_sampling.py +0 -0
  254. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_tabular_utils.py +0 -0
  255. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_uv_sync.py +0 -0
  256. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_wordpress_source.py +0 -0
  257. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_youtube_source.py +0 -0
  258. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/test_youtube_source_integration.py +0 -0
  259. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/utils/test_archive_extraction.py +0 -0
  260. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/utils/test_content_extraction.py +0 -0
  261. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/utils/test_embedded_images.py +0 -0
  262. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/utils/test_file_metadata.py +0 -0
  263. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/utils/test_file_to_images.py +0 -0
  264. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/utils/test_legacy_office.py +0 -0
  265. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/utils/test_transcription.py +0 -0
  266. {classifyre_cli-0.4.74 → classifyre_cli-0.4.76}/tests/utils/test_video_processing.py +0 -0
@@ -0,0 +1,3 @@
1
+ $ uv sync
2
+ Resolved 303 packages in 258ms
3
+ Checked 51 packages in 4ms
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: classifyre-cli
3
- Version: 0.4.74
3
+ Version: 0.4.76
4
4
  Summary: Classifyre CLI — scan and classify unstructured data sources
5
5
  License: MIT
6
6
  Keywords: data,ingestion,metadata,pii,secrets,unstructured
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classifyre/cli",
3
- "version": "0.4.74",
3
+ "version": "0.4.76",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "build": "uv sync",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "classifyre-cli"
3
- version = "0.4.74"
3
+ version = "0.4.76"
4
4
  description = "Classifyre CLI — scan and classify unstructured data sources"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -0,0 +1,53 @@
1
+ """Version stamps that invalidate the scan cache when *our* behaviour changes.
2
+
3
+ The scan cache skips an asset when its content and each detector's configuration
4
+ fingerprint still match the previous successful run. Neither of those changes
5
+ when the change is ours — a new pattern in the PII detector, a new file format in
6
+ ``utils/file_parser.py`` — so the fingerprint carries an explicit version that a
7
+ developer bumps by hand.
8
+
9
+ - Bump ``DETECTOR_ENGINE_VERSION[<type>]`` when a detector's rules, models,
10
+ thresholds or finding shape change. Every asset is then re-scanned by that
11
+ detector alone; the others stay cached.
12
+ - Bump ``PARSER_ENGINE_VERSION`` when text extraction changes what it produces —
13
+ a newly supported format, a different OCR path, a changed truncation rule.
14
+ That changes the *input* to every detector, so it invalidates everything.
15
+
16
+ Forgetting a bump is not a correctness bug, but it is an invisible one: the
17
+ improvement ships and silently does not apply to already-scanned assets. It
18
+ belongs on the release checklist.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import Final
24
+
25
+ PARSER_ENGINE_VERSION: Final[int] = 1
26
+
27
+ DETECTOR_ENGINE_VERSION: Final[dict[str, int]] = {
28
+ "SECRETS": 1,
29
+ "PII": 1,
30
+ "YARA": 1,
31
+ "BROKEN_LINKS": 1,
32
+ "CODE_SECURITY": 1,
33
+ "CUSTOM": 1,
34
+ }
35
+
36
+ # Detectors whose verdict depends on state outside the asset, so identical
37
+ # content plus identical config does not imply an identical result.
38
+ #
39
+ # BROKEN_LINKS resolves URLs over the network: a link that answered 200 last week
40
+ # can 404 today with the document untouched. Caching it would freeze link health
41
+ # at whatever it was on first scan, which is the opposite of what the detector is
42
+ # for. These always run, even on an otherwise fully cached asset.
43
+ NON_CACHEABLE_DETECTOR_TYPES: Final[frozenset[str]] = frozenset({"BROKEN_LINKS"})
44
+
45
+
46
+ def detector_engine_version(detector_type: str) -> int:
47
+ """Engine version for a detector type; 0 for types added without a stamp."""
48
+ return DETECTOR_ENGINE_VERSION.get(detector_type.strip().upper(), 0)
49
+
50
+
51
+ def is_cacheable_detector_type(detector_type: str) -> bool:
52
+ """False when the detector's result can change without the asset changing."""
53
+ return detector_type.strip().upper() not in NON_CACHEABLE_DETECTOR_TYPES
@@ -265,6 +265,16 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
265
265
  worker_pool=worker_pool,
266
266
  )
267
267
 
268
+ from .pipeline.scan_cache import ScanCache
269
+
270
+ scan_cache = ScanCache(recipe, source)
271
+ if scan_cache.enabled:
272
+ logger.info(
273
+ "Scan cache active (verify=%s): unchanged assets and"
274
+ " unchanged detectors will be skipped",
275
+ scan_cache.verify,
276
+ )
277
+
268
278
  # --- Phase 1: Discovery ---
269
279
  source.set_discovery_only(True)
270
280
  all_stubs: list[Any] = []
@@ -286,9 +296,17 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
286
296
  # change_type onto runner_assets rows during ingest, so
287
297
  # the row must exist or the CREATED stamp is lost and
288
298
  # first-run counters report every asset as unchanged.
299
+ #
300
+ # This is also the only point where the stored checksum
301
+ # still describes the *previous* scan — the ingest below
302
+ # overwrites it — so the scan cache reads prior state here
303
+ # or not at all.
289
304
  hashes = [s["hash"] for s in stub_batch if s.get("hash")]
290
305
  if hasattr(sink, "register_discovered_assets") and hashes:
291
- await sink.register_discovered_assets(hashes)
306
+ prior = await sink.register_discovered_assets(
307
+ hashes, include_scan_cache=scan_cache.enabled
308
+ )
309
+ scan_cache.record_prior(prior)
292
310
 
293
311
  await sink.emit_batch(stub_batch, skip_findings=True)
294
312
  output_batch_count += 1
@@ -317,6 +335,8 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
317
335
  max_concurrent,
318
336
  )
319
337
  error_count = 0
338
+ skipped_assets = 0
339
+ skipped_detector_runs = 0
320
340
  chunk_errors: list[str] = []
321
341
 
322
342
  async def _process_one(asset: Any) -> None:
@@ -325,8 +345,45 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
325
345
 
326
346
  async def _process_one_inner(asset: Any) -> None:
327
347
  nonlocal processed_count, error_count
348
+ nonlocal skipped_assets, skipped_detector_runs
328
349
  asset_hash = getattr(asset, "hash", None) or ""
329
350
  try:
351
+ plan = await scan_cache.plan(asset)
352
+ skipped_detector_runs += scan_cache.skipped_detector_runs(plan)
353
+
354
+ if plan.mode == "skip":
355
+ # Report no detector outcomes at all. The API
356
+ # may only resolve a finding whose detector
357
+ # reported OK on this asset, so an empty
358
+ # outcome set is what preserves the findings
359
+ # this run deliberately did not re-check.
360
+ skipped_assets += 1
361
+ processed_count += 1
362
+ logger.info("Cached %s: %s", asset.name, plan.reason)
363
+ prior = plan.prior
364
+ if hasattr(sink, "update_asset_status"):
365
+ await sink.update_asset_status(
366
+ asset_hash,
367
+ "PROCESSED",
368
+ findings_total=prior.findings_total if prior else None,
369
+ findings_by_severity=(
370
+ prior.findings_by_severity if prior else None
371
+ ),
372
+ findings_by_detector=(
373
+ prior.findings_by_detector if prior else None
374
+ ),
375
+ cache_hit=True,
376
+ )
377
+ source.evict_asset_cache(asset_hash)
378
+ return
379
+
380
+ if plan.mode == "partial":
381
+ logger.info(
382
+ "Partially cached %s: running %s",
383
+ asset.name,
384
+ ", ".join(sorted(plan.run_detector_keys)),
385
+ )
386
+
330
387
  if hasattr(sink, "update_asset_status"):
331
388
  await sink.update_asset_status(asset_hash, "PROCESSING")
332
389
 
@@ -355,9 +412,17 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
355
412
  asset,
356
413
  on_findings_flushed=_on_findings_flushed,
357
414
  findings_flush_size=args.detector_flush_batch_size,
415
+ only=(
416
+ plan.run_detector_keys if plan.mode == "partial" else None
417
+ ),
358
418
  )
359
- payload = _asset_to_payload(result)
360
- await sink.emit_batch([payload], skip_findings=False)
419
+
420
+ # Chunks first, then the payload that carries the
421
+ # scan-cache state. A later run reads that state's
422
+ # presence as "this asset finished", so it must not
423
+ # be written while the asset's embeddings are still
424
+ # missing — that would strand them permanently.
425
+ chunks_ok = True
361
426
  if hasattr(sink, "emit_text_chunks"):
362
427
  artifact = pipeline.take_text_artifact(asset_hash)
363
428
  if artifact is not None:
@@ -371,6 +436,7 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
371
436
  # The run fails after phase 2 so operators
372
437
  # cannot mistake missing embeddings for a
373
438
  # healthy completed scan.
439
+ chunks_ok = False
374
440
  detail = f"{asset_hash}: {chunk_exc}"
375
441
  chunk_errors.append(detail)
376
442
  logger.error(
@@ -380,10 +446,40 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
380
446
  chunk_exc,
381
447
  )
382
448
 
449
+ f_total, f_by_sev, f_by_det = _compute_findings_counts(
450
+ result.findings or []
451
+ )
452
+ payload = _asset_to_payload(result)
453
+
454
+ content_hash = plan.content_hash
455
+ if scan_cache.tracks_content_hash and content_hash is None:
456
+ # Bytes are still cached from the detector run,
457
+ # so this digest costs no extra fetch.
458
+ content_hash = await scan_cache.content_digest(asset)
459
+
460
+ state = scan_cache.build_state(
461
+ plan,
462
+ content_hash=content_hash,
463
+ detector_outcomes=(
464
+ result.scan_stats.detector_outcomes
465
+ if result.scan_stats
466
+ else None
467
+ ),
468
+ scan_stats=result.scan_stats,
469
+ available_detector_keys=(
470
+ pipeline.available_detector_cache_keys()
471
+ ),
472
+ completed=chunks_ok,
473
+ findings_total=f_total,
474
+ findings_by_severity=f_by_sev,
475
+ findings_by_detector=f_by_det,
476
+ )
477
+ if state is not None:
478
+ payload["scan_cache"] = state
479
+
480
+ await sink.emit_batch([payload], skip_findings=False)
481
+
383
482
  if hasattr(sink, "update_asset_status"):
384
- f_total, f_by_sev, f_by_det = _compute_findings_counts(
385
- result.findings or []
386
- )
387
483
  await sink.update_asset_status(
388
484
  asset_hash,
389
485
  "PROCESSED",
@@ -409,10 +505,15 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
409
505
  tasks = [asyncio.create_task(_process_one(a)) for a in all_stubs]
410
506
  await asyncio.gather(*tasks, return_exceptions=True)
411
507
  logger.info(
412
- "Phase 2 complete: %d processed, %d errors",
508
+ "Phase 2 complete: %d processed (%d cached), %d errors,"
509
+ " %d detector run(s) skipped",
413
510
  processed_count,
511
+ skipped_assets,
414
512
  error_count,
513
+ skipped_detector_runs,
415
514
  )
515
+ if hasattr(sink, "set_scan_cache_savings"):
516
+ sink.set_scan_cache_savings(skipped_assets, skipped_detector_runs)
416
517
  if chunk_errors:
417
518
  preview = "; ".join(chunk_errors[:3])
418
519
  remaining = len(chunk_errors) - 3
@@ -134,6 +134,34 @@ class SamplingConfig(BaseModel):
134
134
  )
135
135
 
136
136
 
137
+ class Verify(StrEnum):
138
+ """
139
+ How strongly to prove the payload is unchanged. metadata trusts the source-reported checksum (provider ETag/MD5) and skips the download entirely. content downloads the bytes and compares a SHA-256 digest, still skipping parsing, OCR, transcription and detection. auto (default) picks per source: content where the checksum is only mtime and size (local folders), metadata where the provider supplies a content-derived digest.
140
+ """
141
+
142
+ auto = 'auto'
143
+ metadata = 'metadata'
144
+ content = 'content'
145
+
146
+
147
+ class ScanCacheConfig(BaseModel):
148
+ """
149
+ Skips re-processing assets whose content and applicable detector configuration are unchanged since their last fully-successful scan. Supported by file-like sources only (local folders, object storage, cloud drives, email); ignored elsewhere, because a database row can change under the same key with no change signal to compare against. A detector whose configuration changed is always re-run — and only that detector.
150
+ """
151
+
152
+ model_config = ConfigDict(
153
+ extra='forbid',
154
+ )
155
+ enabled: bool | None = Field(
156
+ True,
157
+ description='When enabled (default), an asset whose content and per-detector configuration fingerprints all match its last successful scan is skipped, and its existing findings are carried forward untouched. Disable to re-scan everything on every run.',
158
+ )
159
+ verify: Verify | None = Field(
160
+ 'auto',
161
+ description='How strongly to prove the payload is unchanged. metadata trusts the source-reported checksum (provider ETag/MD5) and skips the download entirely. content downloads the bytes and compares a SHA-256 digest, still skipping parsing, OCR, transcription and detection. auto (default) picks per source: content where the checksum is only mtime and size (local folders), metadata where the provider supplies a content-derived digest.',
162
+ )
163
+
164
+
137
165
  class Requests(BaseModel):
138
166
  model_config = ConfigDict(
139
167
  extra='forbid',
@@ -2110,6 +2138,7 @@ class CoreInput(BaseModel):
2110
2138
  description='Reusable custom detector IDs selected from the custom detector catalog.',
2111
2139
  )
2112
2140
  sampling: SamplingConfig
2141
+ scan_cache: ScanCacheConfig | None = None
2113
2142
  resources: ResourceOverrides | None = None
2114
2143
  cleanup_removed_detector_findings: bool | None = Field(
2115
2144
  True,
@@ -2132,6 +2161,7 @@ class EmailInput(CoreInput):
2132
2161
  description='Reusable custom detector IDs selected from the custom detector catalog.',
2133
2162
  )
2134
2163
  sampling: SamplingConfig
2164
+ scan_cache: ScanCacheConfig | None = None
2135
2165
  resources: ResourceOverrides | None = None
2136
2166
  cleanup_removed_detector_findings: bool | None = Field(
2137
2167
  True,
@@ -2154,6 +2184,7 @@ class S3CompatibleStorageInput(CoreInput):
2154
2184
  description='Reusable custom detector IDs selected from the custom detector catalog.',
2155
2185
  )
2156
2186
  sampling: SamplingConfig
2187
+ scan_cache: ScanCacheConfig | None = None
2157
2188
  resources: ResourceOverrides | None = None
2158
2189
  cleanup_removed_detector_findings: bool | None = Field(
2159
2190
  True,
@@ -2176,6 +2207,7 @@ class LocalFolderInput(CoreInput):
2176
2207
  description='Reusable custom detector IDs selected from the custom detector catalog.',
2177
2208
  )
2178
2209
  sampling: SamplingConfig
2210
+ scan_cache: ScanCacheConfig | None = None
2179
2211
  resources: ResourceOverrides | None = None
2180
2212
  cleanup_removed_detector_findings: bool | None = Field(
2181
2213
  True,
@@ -2198,6 +2230,7 @@ class SandboxInput(CoreInput):
2198
2230
  description='Reusable custom detector IDs selected from the custom detector catalog.',
2199
2231
  )
2200
2232
  sampling: SamplingConfig
2233
+ scan_cache: ScanCacheConfig | None = None
2201
2234
  resources: ResourceOverrides | None = None
2202
2235
  cleanup_removed_detector_findings: bool | None = Field(
2203
2236
  True,
@@ -2220,6 +2253,7 @@ class AzureBlobStorageInput(CoreInput):
2220
2253
  description='Reusable custom detector IDs selected from the custom detector catalog.',
2221
2254
  )
2222
2255
  sampling: SamplingConfig
2256
+ scan_cache: ScanCacheConfig | None = None
2223
2257
  resources: ResourceOverrides | None = None
2224
2258
  cleanup_removed_detector_findings: bool | None = Field(
2225
2259
  True,
@@ -2242,6 +2276,7 @@ class GoogleCloudStorageInput(CoreInput):
2242
2276
  description='Reusable custom detector IDs selected from the custom detector catalog.',
2243
2277
  )
2244
2278
  sampling: SamplingConfig
2279
+ scan_cache: ScanCacheConfig | None = None
2245
2280
  resources: ResourceOverrides | None = None
2246
2281
  cleanup_removed_detector_findings: bool | None = Field(
2247
2282
  True,
@@ -2264,6 +2299,7 @@ class WordPressInput(CoreInput):
2264
2299
  description='Reusable custom detector IDs selected from the custom detector catalog.',
2265
2300
  )
2266
2301
  sampling: SamplingConfig
2302
+ scan_cache: ScanCacheConfig | None = None
2267
2303
  resources: ResourceOverrides | None = None
2268
2304
  cleanup_removed_detector_findings: bool | None = Field(
2269
2305
  True,
@@ -2286,6 +2322,7 @@ class PostgreSQLInput(CoreInput):
2286
2322
  description='Reusable custom detector IDs selected from the custom detector catalog.',
2287
2323
  )
2288
2324
  sampling: SamplingConfig
2325
+ scan_cache: ScanCacheConfig | None = None
2289
2326
  resources: ResourceOverrides | None = None
2290
2327
  cleanup_removed_detector_findings: bool | None = Field(
2291
2328
  True,
@@ -2308,6 +2345,7 @@ class MySQLInput(CoreInput):
2308
2345
  description='Reusable custom detector IDs selected from the custom detector catalog.',
2309
2346
  )
2310
2347
  sampling: SamplingConfig
2348
+ scan_cache: ScanCacheConfig | None = None
2311
2349
  resources: ResourceOverrides | None = None
2312
2350
  cleanup_removed_detector_findings: bool | None = Field(
2313
2351
  True,
@@ -2330,6 +2368,7 @@ class MSSQLInput(CoreInput):
2330
2368
  description='Reusable custom detector IDs selected from the custom detector catalog.',
2331
2369
  )
2332
2370
  sampling: SamplingConfig
2371
+ scan_cache: ScanCacheConfig | None = None
2333
2372
  resources: ResourceOverrides | None = None
2334
2373
  cleanup_removed_detector_findings: bool | None = Field(
2335
2374
  True,
@@ -2352,6 +2391,7 @@ class OracleInput(CoreInput):
2352
2391
  description='Reusable custom detector IDs selected from the custom detector catalog.',
2353
2392
  )
2354
2393
  sampling: SamplingConfig
2394
+ scan_cache: ScanCacheConfig | None = None
2355
2395
  resources: ResourceOverrides | None = None
2356
2396
  cleanup_removed_detector_findings: bool | None = Field(
2357
2397
  True,
@@ -2374,6 +2414,7 @@ class HiveInput(CoreInput):
2374
2414
  description='Reusable custom detector IDs selected from the custom detector catalog.',
2375
2415
  )
2376
2416
  sampling: SamplingConfig
2417
+ scan_cache: ScanCacheConfig | None = None
2377
2418
  resources: ResourceOverrides | None = None
2378
2419
  cleanup_removed_detector_findings: bool | None = Field(
2379
2420
  True,
@@ -2400,6 +2441,7 @@ class DatabricksInput(CoreInput):
2400
2441
  description='Reusable custom detector IDs selected from the custom detector catalog.',
2401
2442
  )
2402
2443
  sampling: SamplingConfig
2444
+ scan_cache: ScanCacheConfig | None = None
2403
2445
  resources: ResourceOverrides | None = None
2404
2446
  cleanup_removed_detector_findings: bool | None = Field(
2405
2447
  True,
@@ -2432,6 +2474,7 @@ class SnowflakeInput(CoreInput):
2432
2474
  description='Reusable custom detector IDs selected from the custom detector catalog.',
2433
2475
  )
2434
2476
  sampling: SamplingConfig
2477
+ scan_cache: ScanCacheConfig | None = None
2435
2478
  resources: ResourceOverrides | None = None
2436
2479
  cleanup_removed_detector_findings: bool | None = Field(
2437
2480
  True,
@@ -2458,6 +2501,7 @@ class MongoDBInput(CoreInput):
2458
2501
  description='Reusable custom detector IDs selected from the custom detector catalog.',
2459
2502
  )
2460
2503
  sampling: SamplingConfig
2504
+ scan_cache: ScanCacheConfig | None = None
2461
2505
  resources: ResourceOverrides | None = None
2462
2506
  cleanup_removed_detector_findings: bool | None = Field(
2463
2507
  True,
@@ -2588,6 +2632,7 @@ class Neo4jInput(CoreInput):
2588
2632
  description='Reusable custom detector IDs selected from the custom detector catalog.',
2589
2633
  )
2590
2634
  sampling: SamplingConfig
2635
+ scan_cache: ScanCacheConfig | None = None
2591
2636
  resources: ResourceOverrides | None = None
2592
2637
  cleanup_removed_detector_findings: bool | None = Field(
2593
2638
  True,
@@ -2614,6 +2659,7 @@ class PowerBIInput(CoreInput):
2614
2659
  description='Reusable custom detector IDs selected from the custom detector catalog.',
2615
2660
  )
2616
2661
  sampling: SamplingConfig
2662
+ scan_cache: ScanCacheConfig | None = None
2617
2663
  resources: ResourceOverrides | None = None
2618
2664
  cleanup_removed_detector_findings: bool | None = Field(
2619
2665
  True,
@@ -2640,6 +2686,7 @@ class TableauInput(CoreInput):
2640
2686
  description='Reusable custom detector IDs selected from the custom detector catalog.',
2641
2687
  )
2642
2688
  sampling: SamplingConfig
2689
+ scan_cache: ScanCacheConfig | None = None
2643
2690
  resources: ResourceOverrides | None = None
2644
2691
  cleanup_removed_detector_findings: bool | None = Field(
2645
2692
  True,
@@ -3051,6 +3098,7 @@ class ConfluenceInput(CoreInput):
3051
3098
  description='Reusable custom detector IDs selected from the custom detector catalog.',
3052
3099
  )
3053
3100
  sampling: SamplingConfig
3101
+ scan_cache: ScanCacheConfig | None = None
3054
3102
  resources: ResourceOverrides | None = None
3055
3103
  cleanup_removed_detector_findings: bool | None = Field(
3056
3104
  True,
@@ -3073,6 +3121,7 @@ class JiraInput(CoreInput):
3073
3121
  description='Reusable custom detector IDs selected from the custom detector catalog.',
3074
3122
  )
3075
3123
  sampling: SamplingConfig
3124
+ scan_cache: ScanCacheConfig | None = None
3076
3125
  resources: ResourceOverrides | None = None
3077
3126
  cleanup_removed_detector_findings: bool | None = Field(
3078
3127
  True,
@@ -3095,6 +3144,7 @@ class ServiceDeskInput(CoreInput):
3095
3144
  description='Reusable custom detector IDs selected from the custom detector catalog.',
3096
3145
  )
3097
3146
  sampling: SamplingConfig
3147
+ scan_cache: ScanCacheConfig | None = None
3098
3148
  resources: ResourceOverrides | None = None
3099
3149
  cleanup_removed_detector_findings: bool | None = Field(
3100
3150
  True,
@@ -3155,6 +3205,7 @@ class SQLiteInput(CoreInput):
3155
3205
  description='Reusable custom detector IDs selected from the custom detector catalog.',
3156
3206
  )
3157
3207
  sampling: SamplingConfig
3208
+ scan_cache: ScanCacheConfig | None = None
3158
3209
  resources: ResourceOverrides | None = None
3159
3210
  cleanup_removed_detector_findings: bool | None = Field(
3160
3211
  True,
@@ -3290,6 +3341,7 @@ class NotionInput(CoreInput):
3290
3341
  description='Reusable custom detector IDs selected from the custom detector catalog.',
3291
3342
  )
3292
3343
  sampling: SamplingConfig
3344
+ scan_cache: ScanCacheConfig | None = None
3293
3345
  resources: ResourceOverrides | None = None
3294
3346
  cleanup_removed_detector_findings: bool | None = Field(
3295
3347
  True,
@@ -3537,6 +3589,7 @@ class KafkaInput(CoreInput):
3537
3589
  description='Reusable custom detector IDs selected from the custom detector catalog.',
3538
3590
  )
3539
3591
  sampling: SamplingConfig
3592
+ scan_cache: ScanCacheConfig | None = None
3540
3593
  resources: ResourceOverrides | None = None
3541
3594
  cleanup_removed_detector_findings: bool | None = Field(
3542
3595
  True,
@@ -3671,6 +3724,7 @@ class ElasticsearchInput(CoreInput):
3671
3724
  description='Reusable custom detector IDs selected from the custom detector catalog.',
3672
3725
  )
3673
3726
  sampling: SamplingConfig
3727
+ scan_cache: ScanCacheConfig | None = None
3674
3728
  resources: ResourceOverrides | None = None
3675
3729
  cleanup_removed_detector_findings: bool | None = Field(
3676
3730
  True,
@@ -3705,6 +3759,7 @@ class OpenSearchInput(CoreInput):
3705
3759
  description='Reusable custom detector IDs selected from the custom detector catalog.',
3706
3760
  )
3707
3761
  sampling: SamplingConfig
3762
+ scan_cache: ScanCacheConfig | None = None
3708
3763
  resources: ResourceOverrides | None = None
3709
3764
  cleanup_removed_detector_findings: bool | None = Field(
3710
3765
  True,
@@ -3797,6 +3852,7 @@ class MeilisearchInput(CoreInput):
3797
3852
  description='Reusable custom detector IDs selected from the custom detector catalog.',
3798
3853
  )
3799
3854
  sampling: SamplingConfig
3855
+ scan_cache: ScanCacheConfig | None = None
3800
3856
  resources: ResourceOverrides | None = None
3801
3857
  cleanup_removed_detector_findings: bool | None = Field(
3802
3858
  True,
@@ -4037,6 +4093,7 @@ class Microsoft365Input(CoreInput):
4037
4093
  description='Reusable custom detector IDs selected from the custom detector catalog.',
4038
4094
  )
4039
4095
  sampling: SamplingConfig
4096
+ scan_cache: ScanCacheConfig | None = None
4040
4097
  resources: ResourceOverrides | None = None
4041
4098
  cleanup_removed_detector_findings: bool | None = Field(
4042
4099
  True,
@@ -4181,6 +4238,7 @@ class GoogleWorkspaceInput(CoreInput):
4181
4238
  description='Reusable custom detector IDs selected from the custom detector catalog.',
4182
4239
  )
4183
4240
  sampling: SamplingConfig
4241
+ scan_cache: ScanCacheConfig | None = None
4184
4242
  resources: ResourceOverrides | None = None
4185
4243
  cleanup_removed_detector_findings: bool | None = Field(
4186
4244
  True,
@@ -4393,6 +4451,7 @@ class DropboxInput(CoreInput):
4393
4451
  description='Reusable custom detector IDs selected from the custom detector catalog.',
4394
4452
  )
4395
4453
  sampling: SamplingConfig
4454
+ scan_cache: ScanCacheConfig | None = None
4396
4455
  resources: ResourceOverrides | None = None
4397
4456
  cleanup_removed_detector_findings: bool | None = Field(
4398
4457
  True,
@@ -4441,6 +4500,7 @@ class YouTubeInput(CoreInput):
4441
4500
  description='Reusable custom detector IDs selected from the custom detector catalog.',
4442
4501
  )
4443
4502
  sampling: SamplingConfig
4503
+ scan_cache: ScanCacheConfig | None = None
4444
4504
  resources: ResourceOverrides | None = None
4445
4505
  cleanup_removed_detector_findings: bool | None = Field(
4446
4506
  True,
@@ -4473,6 +4533,7 @@ class SlackInput(CoreInput):
4473
4533
  description='Reusable custom detector IDs selected from the custom detector catalog.',
4474
4534
  )
4475
4535
  sampling: SamplingConfig
4536
+ scan_cache: ScanCacheConfig | None = None
4476
4537
  resources: ResourceOverrides | None = None
4477
4538
  cleanup_removed_detector_findings: bool | None = Field(
4478
4539
  True,
@@ -4503,6 +4564,7 @@ class DeltaLakeInput(CoreInput):
4503
4564
  description='Reusable custom detector IDs selected from the custom detector catalog.',
4504
4565
  )
4505
4566
  sampling: SamplingConfig
4567
+ scan_cache: ScanCacheConfig | None = None
4506
4568
  resources: ResourceOverrides | None = None
4507
4569
  cleanup_removed_detector_findings: bool | None = Field(
4508
4570
  True,
@@ -4533,6 +4595,7 @@ class IcebergInput(CoreInput):
4533
4595
  description='Reusable custom detector IDs selected from the custom detector catalog.',
4534
4596
  )
4535
4597
  sampling: SamplingConfig
4598
+ scan_cache: ScanCacheConfig | None = None
4536
4599
  resources: ResourceOverrides | None = None
4537
4600
  cleanup_removed_detector_findings: bool | None = Field(
4538
4601
  True,
@@ -211,6 +211,53 @@ class DetectorOutcome(BaseModel):
211
211
  )
212
212
 
213
213
 
214
+ class ScanCacheState(BaseModel):
215
+ """
216
+ What a later run needs in order to prove this asset can be skipped, and what it should report when it does. Only a state with complete=true is eligible for reuse. A detector that raised or failed to initialize is omitted from the fingerprint map so it is retried next run rather than silently trusted.
217
+ """
218
+
219
+ complete: bool = Field(
220
+ ...,
221
+ description='True only after findings, extraction and text chunks all completed. False is an explicit tombstone that invalidates older reusable state.',
222
+ title='Complete',
223
+ )
224
+ findings_total: int | None = Field(
225
+ None,
226
+ description='Findings this scan saw on the asset, carried forward so a later skipped run reports the same total instead of zero',
227
+ title='Findings Total',
228
+ )
229
+ findings_by_severity: dict[str, int] | None = Field(
230
+ None,
231
+ description='Finding counts per severity, carried forward to a skipped run',
232
+ title='Findings By Severity',
233
+ )
234
+ findings_by_detector: dict[str, dict[str, int]] | None = Field(
235
+ None,
236
+ description='Finding counts per detector and severity, carried forward to a skipped run',
237
+ title='Findings By Detector',
238
+ )
239
+ empty_text: bool | None = Field(
240
+ None,
241
+ description='Whether text extraction yielded nothing, carried forward so a skipped run does not distort text-coverage reporting',
242
+ title='Empty Text',
243
+ )
244
+ text_extraction_status: str | None = Field(
245
+ None,
246
+ description='Text extraction outcome, carried forward to a skipped run',
247
+ title='Text Extraction Status',
248
+ )
249
+ content_hash: str | None = Field(
250
+ None,
251
+ description='SHA-256 of the bytes that were actually scanned. Null when the source proved sameness from a provider-supplied digest without downloading.',
252
+ title='Content Hash',
253
+ )
254
+ detectors: dict[str, str] | None = Field(
255
+ {},
256
+ description='Fingerprint of the configuration each detector last succeeded with, keyed by detector type or CUSTOM::<custom_detector_key>. A key whose fingerprint still matches on a later run means that detector would produce identical findings and can be skipped.',
257
+ title='Detectors',
258
+ )
259
+
260
+
214
261
  class ScanStats(BaseModel):
215
262
  """
216
263
  Statistics about detector scan for an asset
@@ -307,3 +354,8 @@ class SingleAssetScanResults(BaseModel):
307
354
  description='Source-specific asset metadata using normalized keys (size_bytes, row_count, etc.) where applicable',
308
355
  title='Metadata',
309
356
  )
357
+ scan_cache: ScanCacheState | None = Field(
358
+ None,
359
+ description='Scan-cache state to persist for this asset, letting a later run skip work that would produce identical results',
360
+ title='Scan Cache',
361
+ )