classifyre-cli 0.4.69__tar.gz → 0.4.71__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.
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/.turbo/turbo-build.log +1 -1
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/PKG-INFO +1 -1
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/package.json +1 -1
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/pyproject.toml +1 -1
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/main.py +6 -1
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/models/generated_input.py +56 -27
- classifyre_cli-0.4.71/src/sources/dropbox/auth.py +254 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/dropbox/source.py +65 -9
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/kafka/source.py +194 -15
- classifyre_cli-0.4.71/tests/test_dropbox_auth_command.py +172 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_dropbox_source.py +75 -6
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_kafka_source.py +132 -9
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/uv.lock +11 -11
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/.gitignore +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/.python-version +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/README.md +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/main.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/scripts/generate_models.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/config.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/base.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/broken_links/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/broken_links/detector.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/config.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/custom/detector.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/custom/extractor.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/custom/runners/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/custom/runners/_base.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/custom/runners/_factory.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/custom/runners/_gliner2.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/custom/runners/_image_classification.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/custom/runners/_llm.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/custom/runners/_object_detection.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/custom/runners/_regex.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/custom/runners/_text_classification.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/custom/trainer.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/dependencies.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/pii/detector.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/secrets/detector.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/threat/code_security_detector.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/detectors/threat/yara_detector.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/file_evaluation/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/file_evaluation/runner.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/models/generated_detectors.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/models/generated_single_asset_scan_results.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/outputs/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/outputs/base.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/outputs/console.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/outputs/factory.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/outputs/file.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/outputs/rest.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/pipeline/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/pipeline/content_provider.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/pipeline/detector_pipeline.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/pipeline/parsed_content_provider.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/pipeline/text_artifact.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/pipeline/worker_pool.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/asset_metadata.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/atlassian_common.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/azure_blob_storage/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/azure_blob_storage/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/base.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/confluence/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/confluence/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/databricks/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/databricks/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/delta_lake/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/delta_lake/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/dependencies.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/dropbox/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/elasticsearch/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/elasticsearch/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/email/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/email/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/google_cloud_storage/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/google_cloud_storage/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/google_workspace/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/google_workspace/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/hive/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/hive/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/iceberg/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/iceberg/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/jira/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/jira/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/kafka/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/kafka/rest.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/lakehouse_base.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/local_folder/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/local_folder/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/meilisearch/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/meilisearch/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/microsoft_365/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/microsoft_365/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/mongodb/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/mongodb/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/mssql/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/mssql/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/mysql/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/mysql/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/neo4j/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/neo4j/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/notion/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/notion/client.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/notion/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/object_storage/base.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/opensearch/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/opensearch/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/oracle/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/oracle/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/postgresql/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/postgresql/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/powerbi/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/powerbi/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/recipe_normalizer.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/s3_client.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/s3_compatible_storage/README.md +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/s3_compatible_storage/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/s3_compatible_storage/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/sandbox/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/sandbox/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/search_engine_base.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/servicedesk/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/servicedesk/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/slack/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/slack/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/snowflake/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/snowflake/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/sqlite/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/sqlite/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/tableau/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/tableau/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/tabular_base.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/tabular_utils.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/wordpress/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/wordpress/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/youtube/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/sources/youtube/source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/telemetry.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/utils/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/utils/archive_extraction.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/utils/content_extraction.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/utils/dependency_groups.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/utils/embedded_images.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/utils/file_metadata.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/utils/file_parser.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/utils/file_to_images.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/utils/hashing.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/utils/legacy_office.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/utils/resources.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/utils/transcription.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/utils/uv_sync.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/utils/validation.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/src/utils/video_processing.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/_lakehouse_fakes.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/conftest.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/conftest.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/custom/conftest.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/custom/test_gliner2_classification.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/custom/test_invoice_extraction.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/custom/test_llm_runner.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/custom/test_pipeline_integration.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/custom/test_regex_runner.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/custom/test_runner_hardening.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/custom/test_transformer_runners.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/pii/conftest.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/pii/sample_invoice.pdf +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/pii/test_pii_chunking.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/pii/test_pii_detector.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/pii/test_pii_severity.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/secrets/test_secrets_detector.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/test_base_detector.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/test_detector_catalog_commercial.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/test_detector_pipeline_types.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/test_detector_schema_examples.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/test_detector_types.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/test_phase2_detectors.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/test_registry.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/threat/test_code_security_detector.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/detectors/threat/test_yara_detector.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/integration/test_wordpress_links_assets.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/pipeline/test_detector_outcomes.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/pipeline/test_detector_pipeline.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/pipeline/test_worker_pool.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_assets_metadata_catalog.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_azure_blob_storage_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_base_source_attachment.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_base_source_sampling.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_config.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_confluence_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_custom_extractor.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_databricks_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_delta_lake_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_dependency_groups.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_elasticsearch_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_email_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_file_evaluation_runner.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_google_cloud_storage_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_google_workspace_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_hashing.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_hive_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_iceberg_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_jira_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_local_folder_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_main.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_meilisearch_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_microsoft_365_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_mongodb_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_mssql_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_mysql_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_neo4j_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_notion_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_opensearch_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_oracle_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_outputs.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_postgresql_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_powerbi_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_recipe_normalizer.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_s3_compatible_storage_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_sampling_automatic.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_sandbox_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_servicedesk_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_slack_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_snowflake_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_source_dependency_groups.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_source_strategy_conformance.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_sqlite_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_tableau_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_tabular_automatic_sampling.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_tabular_utils.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_uv_sync.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_wordpress_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_youtube_source.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/test_youtube_source_integration.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/utils/test_archive_extraction.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/utils/test_content_extraction.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/utils/test_embedded_images.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/utils/test_file_metadata.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/utils/test_file_parser.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/utils/test_file_to_images.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/utils/test_legacy_office.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/utils/test_transcription.py +0 -0
- {classifyre_cli-0.4.69 → classifyre_cli-0.4.71}/tests/utils/test_video_processing.py +0 -0
|
@@ -10,6 +10,7 @@ from typing import Any, cast
|
|
|
10
10
|
|
|
11
11
|
from .outputs import create_output_sink
|
|
12
12
|
from .sources import get_source, list_available_sources
|
|
13
|
+
from .sources.dropbox.auth import add_dropbox_auth_arguments, run_dropbox_auth_command
|
|
13
14
|
from .utils.validation import validate_input, validate_test_connection
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
@@ -586,7 +587,7 @@ def main() -> None:
|
|
|
586
587
|
parser = argparse.ArgumentParser(description="Classifyre Metadata Extraction CLI")
|
|
587
588
|
parser.add_argument(
|
|
588
589
|
"command",
|
|
589
|
-
choices=["test", "extract", "discover", "evaluate-file", "train"],
|
|
590
|
+
choices=["test", "extract", "discover", "evaluate-file", "train", "dropbox-auth"],
|
|
590
591
|
help="Command to run",
|
|
591
592
|
)
|
|
592
593
|
parser.add_argument(
|
|
@@ -667,6 +668,7 @@ def main() -> None:
|
|
|
667
668
|
default=None,
|
|
668
669
|
help="Max assets processed concurrently in Phase 2. Controls DB connection usage. Defaults to pool_workers*2 (env: CLASSIFYRE_MAX_CONCURRENT_ASSETS)",
|
|
669
670
|
)
|
|
671
|
+
add_dropbox_auth_arguments(parser)
|
|
670
672
|
|
|
671
673
|
args = parser.parse_args()
|
|
672
674
|
|
|
@@ -695,6 +697,9 @@ def main() -> None:
|
|
|
695
697
|
if args.debug:
|
|
696
698
|
logging.getLogger().setLevel(logging.DEBUG)
|
|
697
699
|
|
|
700
|
+
if args.command == "dropbox-auth":
|
|
701
|
+
sys.exit(run_dropbox_auth_command(args))
|
|
702
|
+
|
|
698
703
|
if args.command == "evaluate-file":
|
|
699
704
|
run_evaluate_file_command(args)
|
|
700
705
|
return
|
|
@@ -3461,27 +3461,31 @@ class SASLUsernamePassword1(BaseModel):
|
|
|
3461
3461
|
)
|
|
3462
3462
|
sasl_username: str = Field(..., description='SASL username')
|
|
3463
3463
|
sasl_password: str = Field(..., description='SASL password')
|
|
3464
|
-
|
|
3464
|
+
ca_certificate: str | None = Field(
|
|
3465
3465
|
None,
|
|
3466
|
-
description="PEM-encoded CA certificate used to verify the broker's TLS certificate. Required for private or self-signed CAs; leave empty to trust the system CA bundle.",
|
|
3466
|
+
description="PEM-encoded CA certificate used to verify the broker's TLS certificate — the provider's ca.pem. Required for private or self-signed CAs; leave empty to trust the system CA bundle.",
|
|
3467
3467
|
)
|
|
3468
3468
|
|
|
3469
3469
|
|
|
3470
3470
|
class ClientCertificateMTLS1(BaseModel):
|
|
3471
3471
|
"""
|
|
3472
|
-
mTLS credentials
|
|
3472
|
+
mTLS credentials, named as the providers name them: Aiven's access key / access certificate / CA certificate, Confluent's client key / client certificate. Paste each PEM block whole, including its BEGIN and END lines.
|
|
3473
3473
|
"""
|
|
3474
3474
|
|
|
3475
3475
|
model_config = ConfigDict(
|
|
3476
3476
|
extra='forbid',
|
|
3477
3477
|
)
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3478
|
+
access_key: str = Field(
|
|
3479
|
+
...,
|
|
3480
|
+
description='PEM-encoded client private key — the block starting with -----BEGIN PRIVATE KEY----- (Aiven: service.key)',
|
|
3481
3481
|
)
|
|
3482
|
-
|
|
3482
|
+
access_certificate: str = Field(
|
|
3483
|
+
...,
|
|
3484
|
+
description='PEM-encoded client certificate — the block starting with -----BEGIN CERTIFICATE----- (Aiven: service.cert)',
|
|
3485
|
+
)
|
|
3486
|
+
ca_certificate: str | None = Field(
|
|
3483
3487
|
None,
|
|
3484
|
-
description="PEM-encoded CA certificate used to verify the broker's TLS certificate. Required for private or self-signed CAs; leave empty to trust the system CA bundle.",
|
|
3488
|
+
description="PEM-encoded CA certificate used to verify the broker's TLS certificate — the provider's ca.pem. Required for private or self-signed CAs; leave empty to trust the system CA bundle.",
|
|
3485
3489
|
)
|
|
3486
3490
|
|
|
3487
3491
|
|
|
@@ -4221,39 +4225,43 @@ class GoogleWorkspaceInput(CoreInput):
|
|
|
4221
4225
|
)
|
|
4222
4226
|
|
|
4223
4227
|
|
|
4224
|
-
class
|
|
4228
|
+
class DropboxOAuthRefreshTokenRecommended(BaseModel):
|
|
4225
4229
|
"""
|
|
4226
|
-
|
|
4230
|
+
Recommended. An OAuth app key and secret plus a refresh token, which never expires — the client mints a fresh access token on every run. Obtain the refresh token once with `classifyre dropbox-auth --app-key <key> --app-secret <secret>`; it prints a URL you open in your own browser and exchanges the code it gives you.
|
|
4227
4231
|
"""
|
|
4228
4232
|
|
|
4229
4233
|
model_config = ConfigDict(
|
|
4230
4234
|
extra='forbid',
|
|
4231
4235
|
)
|
|
4232
|
-
auth_method: Literal['
|
|
4236
|
+
auth_method: Literal['oauth']
|
|
4237
|
+
app_key: str = Field(
|
|
4238
|
+
..., description='Dropbox app key (App Console → Settings → App key)'
|
|
4239
|
+
)
|
|
4233
4240
|
|
|
4234
4241
|
|
|
4235
|
-
class
|
|
4242
|
+
class DropboxOAuthWithPKCENoAppSecret(BaseModel):
|
|
4236
4243
|
"""
|
|
4237
|
-
|
|
4244
|
+
Same as the OAuth refresh token flow but for apps that hold no secret: the authorization is proved with PKCE, so only the app key is needed to refresh. Obtain the refresh token with `classifyre dropbox-auth --app-key <key> --pkce`.
|
|
4238
4245
|
"""
|
|
4239
4246
|
|
|
4240
4247
|
model_config = ConfigDict(
|
|
4241
4248
|
extra='forbid',
|
|
4242
4249
|
)
|
|
4243
|
-
auth_method: Literal['
|
|
4250
|
+
auth_method: Literal['oauth_pkce']
|
|
4244
4251
|
app_key: str = Field(
|
|
4245
|
-
...,
|
|
4246
|
-
description='Dropbox app key (client ID) of the OAuth app that issued the refresh token',
|
|
4252
|
+
..., description='Dropbox app key (App Console → Settings → App key)'
|
|
4247
4253
|
)
|
|
4248
4254
|
|
|
4249
4255
|
|
|
4250
|
-
class
|
|
4256
|
+
class DropboxAccessTokenShortLivedTestingOnly(BaseModel):
|
|
4257
|
+
"""
|
|
4258
|
+
Testing only. The token generated by the App Console button is short-lived and stops working about four hours later with `invalid_access_token`, so scheduled scans will fail. Use one of the refresh-token methods for anything recurring.
|
|
4259
|
+
"""
|
|
4260
|
+
|
|
4251
4261
|
model_config = ConfigDict(
|
|
4252
4262
|
extra='forbid',
|
|
4253
4263
|
)
|
|
4254
|
-
|
|
4255
|
-
..., description='Dropbox OAuth 2.0 access token (sl.… for short-lived tokens)'
|
|
4256
|
-
)
|
|
4264
|
+
auth_method: Literal['access_token']
|
|
4257
4265
|
|
|
4258
4266
|
|
|
4259
4267
|
class DropboxMaskedOAuth(BaseModel):
|
|
@@ -4261,12 +4269,31 @@ class DropboxMaskedOAuth(BaseModel):
|
|
|
4261
4269
|
extra='forbid',
|
|
4262
4270
|
)
|
|
4263
4271
|
app_secret: str = Field(
|
|
4272
|
+
..., description='Dropbox app secret (App Console → Settings → App secret)'
|
|
4273
|
+
)
|
|
4274
|
+
refresh_token: str = Field(
|
|
4264
4275
|
...,
|
|
4265
|
-
description='
|
|
4276
|
+
description='Long-lived refresh token issued with token_access_type=offline',
|
|
4277
|
+
)
|
|
4278
|
+
|
|
4279
|
+
|
|
4280
|
+
class DropboxMaskedOAuthPkce(BaseModel):
|
|
4281
|
+
model_config = ConfigDict(
|
|
4282
|
+
extra='forbid',
|
|
4266
4283
|
)
|
|
4267
4284
|
refresh_token: str = Field(
|
|
4268
4285
|
...,
|
|
4269
|
-
description='
|
|
4286
|
+
description='Long-lived refresh token issued by the PKCE flow with token_access_type=offline',
|
|
4287
|
+
)
|
|
4288
|
+
|
|
4289
|
+
|
|
4290
|
+
class DropboxMaskedAccessToken(BaseModel):
|
|
4291
|
+
model_config = ConfigDict(
|
|
4292
|
+
extra='forbid',
|
|
4293
|
+
)
|
|
4294
|
+
access_token: str = Field(
|
|
4295
|
+
...,
|
|
4296
|
+
description='Short-lived Dropbox access token (starts with sl.). Expires roughly four hours after it is generated.',
|
|
4270
4297
|
)
|
|
4271
4298
|
|
|
4272
4299
|
|
|
@@ -4386,11 +4413,13 @@ class DropboxInput(CoreInput):
|
|
|
4386
4413
|
type: Literal['DROPBOX'] | None = Field(
|
|
4387
4414
|
None, description='Type of the asset or source'
|
|
4388
4415
|
)
|
|
4389
|
-
required:
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4416
|
+
required: (
|
|
4417
|
+
DropboxOAuthRefreshTokenRecommended
|
|
4418
|
+
| DropboxOAuthWithPKCENoAppSecret
|
|
4419
|
+
| DropboxAccessTokenShortLivedTestingOnly
|
|
4420
|
+
) = Field(..., title='DropboxRequired')
|
|
4421
|
+
masked: DropboxMaskedOAuth | DropboxMaskedOAuthPkce | DropboxMaskedAccessToken = (
|
|
4422
|
+
Field(..., title='DropboxMasked')
|
|
4394
4423
|
)
|
|
4395
4424
|
optional: DropboxOptional | None = None
|
|
4396
4425
|
detectors: list[Detector] | None = Field(
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""One-off Dropbox authorization helper.
|
|
2
|
+
|
|
3
|
+
Dropbox only hands out long-lived credentials through an interactive OAuth
|
|
4
|
+
authorization. A scan runner has no browser and no logged-in user, so the
|
|
5
|
+
exchange has to happen once, up front, on a machine where a human is present.
|
|
6
|
+
|
|
7
|
+
``DropboxOAuth2FlowNoRedirect`` is built exactly for that: it needs no redirect
|
|
8
|
+
URI and no callback server. It prints a URL, the operator opens it in their own
|
|
9
|
+
browser, Dropbox shows them a short authorization code, and the code is pasted
|
|
10
|
+
back here and exchanged for a refresh token. Only that refresh token — which
|
|
11
|
+
does not expire — is ever stored on a source.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from ..dependencies import require_module
|
|
22
|
+
|
|
23
|
+
# Scopes each mode needs. Dropbox binds scopes to the refresh token at
|
|
24
|
+
# authorization time, so anything missing here means a `missing_scope` error at
|
|
25
|
+
# scan time and a fresh authorization to fix it.
|
|
26
|
+
INDIVIDUAL_SCOPES = [
|
|
27
|
+
"account_info.read",
|
|
28
|
+
"files.metadata.read",
|
|
29
|
+
"files.content.read",
|
|
30
|
+
"sharing.read",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
TEAM_SCOPES = [
|
|
34
|
+
*INDIVIDUAL_SCOPES,
|
|
35
|
+
"team_info.read",
|
|
36
|
+
"members.read",
|
|
37
|
+
"team_data.member",
|
|
38
|
+
"team_data.team_space",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def default_scopes(*, team: bool) -> list[str]:
|
|
43
|
+
return list(TEAM_SCOPES if team else INDIVIDUAL_SCOPES)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def build_flow(
|
|
47
|
+
*,
|
|
48
|
+
app_key: str,
|
|
49
|
+
app_secret: str | None,
|
|
50
|
+
scopes: list[str],
|
|
51
|
+
use_pkce: bool,
|
|
52
|
+
) -> Any:
|
|
53
|
+
"""Create the no-redirect authorization flow.
|
|
54
|
+
|
|
55
|
+
``token_access_type="offline"`` is what makes Dropbox return a refresh
|
|
56
|
+
token; without it the exchange yields only a four-hour access token.
|
|
57
|
+
"""
|
|
58
|
+
oauth = require_module(
|
|
59
|
+
module_name="dropbox.oauth",
|
|
60
|
+
source_name="Dropbox",
|
|
61
|
+
uv_groups=["dropbox"],
|
|
62
|
+
detail="Dropbox authorization requires the official Dropbox SDK.",
|
|
63
|
+
)
|
|
64
|
+
return oauth.DropboxOAuth2FlowNoRedirect(
|
|
65
|
+
app_key,
|
|
66
|
+
consumer_secret=None if use_pkce else app_secret,
|
|
67
|
+
token_access_type="offline",
|
|
68
|
+
scope=scopes,
|
|
69
|
+
use_pkce=use_pkce,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def verify_credentials(
|
|
74
|
+
*,
|
|
75
|
+
app_key: str,
|
|
76
|
+
app_secret: str | None,
|
|
77
|
+
refresh_token: str,
|
|
78
|
+
team: bool,
|
|
79
|
+
) -> str:
|
|
80
|
+
"""Prove the refresh token works before printing it as a recipe."""
|
|
81
|
+
dropbox_module = require_module(
|
|
82
|
+
module_name="dropbox",
|
|
83
|
+
source_name="Dropbox",
|
|
84
|
+
uv_groups=["dropbox"],
|
|
85
|
+
detail="Dropbox authorization requires the official Dropbox SDK.",
|
|
86
|
+
)
|
|
87
|
+
kwargs: dict[str, Any] = {
|
|
88
|
+
"oauth2_refresh_token": refresh_token,
|
|
89
|
+
"app_key": app_key,
|
|
90
|
+
"user_agent": "classifyre",
|
|
91
|
+
}
|
|
92
|
+
if app_secret:
|
|
93
|
+
kwargs["app_secret"] = app_secret
|
|
94
|
+
|
|
95
|
+
if team:
|
|
96
|
+
client = dropbox_module.DropboxTeam(**kwargs)
|
|
97
|
+
info = client.team_get_info()
|
|
98
|
+
return (
|
|
99
|
+
f"team '{getattr(info, 'name', '')}' ({getattr(info, 'num_licensed_users', '?')} seats)"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
client = dropbox_module.Dropbox(**kwargs)
|
|
103
|
+
account = client.users_get_current_account()
|
|
104
|
+
display_name = getattr(getattr(account, "name", None), "display_name", "") or ""
|
|
105
|
+
return f"{display_name} <{getattr(account, 'email', '')}>"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def build_recipe_credentials(
|
|
109
|
+
*,
|
|
110
|
+
app_key: str,
|
|
111
|
+
app_secret: str | None,
|
|
112
|
+
refresh_token: str,
|
|
113
|
+
use_pkce: bool,
|
|
114
|
+
) -> dict[str, Any]:
|
|
115
|
+
"""The `required`/`masked` blocks to paste into a DROPBOX source."""
|
|
116
|
+
if use_pkce:
|
|
117
|
+
return {
|
|
118
|
+
"required": {"auth_method": "oauth_pkce", "app_key": app_key},
|
|
119
|
+
"masked": {"refresh_token": refresh_token},
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
"required": {"auth_method": "oauth", "app_key": app_key},
|
|
123
|
+
"masked": {"app_secret": app_secret or "", "refresh_token": refresh_token},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def run_dropbox_auth_command(args: argparse.Namespace) -> int:
|
|
128
|
+
app_key = str(getattr(args, "app_key", "") or "").strip()
|
|
129
|
+
app_secret = str(getattr(args, "app_secret", "") or "").strip() or None
|
|
130
|
+
use_pkce = bool(getattr(args, "pkce", False))
|
|
131
|
+
team = bool(getattr(args, "team", False))
|
|
132
|
+
|
|
133
|
+
if not app_key:
|
|
134
|
+
print("--app-key is required (Dropbox App Console → Settings → App key)")
|
|
135
|
+
return 1
|
|
136
|
+
if not use_pkce and not app_secret:
|
|
137
|
+
print(
|
|
138
|
+
"--app-secret is required unless --pkce is used.\n"
|
|
139
|
+
"Use --pkce for an app that cannot hold a secret; it refreshes with the app key alone."
|
|
140
|
+
)
|
|
141
|
+
return 1
|
|
142
|
+
|
|
143
|
+
scopes = list(getattr(args, "scope", None) or []) or default_scopes(team=team)
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
flow = build_flow(
|
|
147
|
+
app_key=app_key,
|
|
148
|
+
app_secret=app_secret,
|
|
149
|
+
scopes=scopes,
|
|
150
|
+
use_pkce=use_pkce,
|
|
151
|
+
)
|
|
152
|
+
authorize_url = flow.start()
|
|
153
|
+
except Exception as exc:
|
|
154
|
+
print(f"Failed to start the Dropbox authorization flow: {exc}")
|
|
155
|
+
return 1
|
|
156
|
+
|
|
157
|
+
print()
|
|
158
|
+
print("Dropbox authorization — this runs once; the runner never needs a browser.")
|
|
159
|
+
print(f" Mode: {'PKCE (no app secret)' if use_pkce else 'app key + secret'}")
|
|
160
|
+
print(f" Target: {'Dropbox Business team' if team else 'individual account'}")
|
|
161
|
+
print(f" Scopes: {' '.join(scopes)}")
|
|
162
|
+
print()
|
|
163
|
+
print("1. Open this URL in your browser:")
|
|
164
|
+
print(f" {authorize_url}")
|
|
165
|
+
print('2. Sign in if needed, then click "Allow".')
|
|
166
|
+
print("3. Copy the authorization code Dropbox shows you.")
|
|
167
|
+
print()
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
auth_code = input("Authorization code: ").strip()
|
|
171
|
+
except (EOFError, KeyboardInterrupt):
|
|
172
|
+
print("\nAborted.")
|
|
173
|
+
return 1
|
|
174
|
+
|
|
175
|
+
if not auth_code:
|
|
176
|
+
print("No authorization code entered.")
|
|
177
|
+
return 1
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
result = flow.finish(auth_code)
|
|
181
|
+
except Exception as exc:
|
|
182
|
+
print(f"Failed to exchange the authorization code: {exc}")
|
|
183
|
+
return 1
|
|
184
|
+
|
|
185
|
+
refresh_token = getattr(result, "refresh_token", None)
|
|
186
|
+
if not refresh_token:
|
|
187
|
+
print(
|
|
188
|
+
"Dropbox returned no refresh token. The app must be authorized with "
|
|
189
|
+
"token_access_type=offline; re-run this command."
|
|
190
|
+
)
|
|
191
|
+
return 1
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
identity = verify_credentials(
|
|
195
|
+
app_key=app_key,
|
|
196
|
+
app_secret=app_secret,
|
|
197
|
+
refresh_token=refresh_token,
|
|
198
|
+
team=team,
|
|
199
|
+
)
|
|
200
|
+
except Exception as exc:
|
|
201
|
+
print(f"Refresh token issued, but the verification call failed: {exc}")
|
|
202
|
+
if team:
|
|
203
|
+
print(
|
|
204
|
+
"For a team scan the Dropbox app must use a team access type "
|
|
205
|
+
"('Team member file access')."
|
|
206
|
+
)
|
|
207
|
+
return 1
|
|
208
|
+
|
|
209
|
+
credentials = build_recipe_credentials(
|
|
210
|
+
app_key=app_key,
|
|
211
|
+
app_secret=app_secret,
|
|
212
|
+
refresh_token=refresh_token,
|
|
213
|
+
use_pkce=use_pkce,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
print()
|
|
217
|
+
print(f"Authorized as {identity}.")
|
|
218
|
+
print("Paste these into the Dropbox source configuration:")
|
|
219
|
+
print()
|
|
220
|
+
print(json.dumps(credentials, indent=2))
|
|
221
|
+
print()
|
|
222
|
+
print("The refresh token does not expire. Treat it as a secret.")
|
|
223
|
+
return 0
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def add_dropbox_auth_arguments(parser: argparse.ArgumentParser) -> None:
|
|
227
|
+
parser.add_argument("--app-key", default=None, help="Dropbox app key (dropbox-auth only)")
|
|
228
|
+
parser.add_argument(
|
|
229
|
+
"--app-secret",
|
|
230
|
+
default=None,
|
|
231
|
+
help="Dropbox app secret; omit when using --pkce (dropbox-auth only)",
|
|
232
|
+
)
|
|
233
|
+
parser.add_argument(
|
|
234
|
+
"--pkce",
|
|
235
|
+
action="store_true",
|
|
236
|
+
help="Authorize with PKCE so no app secret is needed (dropbox-auth only)",
|
|
237
|
+
)
|
|
238
|
+
parser.add_argument(
|
|
239
|
+
"--team",
|
|
240
|
+
action="store_true",
|
|
241
|
+
help="Authorize a Dropbox Business team app and request team scopes (dropbox-auth only)",
|
|
242
|
+
)
|
|
243
|
+
parser.add_argument(
|
|
244
|
+
"--scope",
|
|
245
|
+
action="append",
|
|
246
|
+
default=None,
|
|
247
|
+
help="Override a requested OAuth scope; repeatable (dropbox-auth only)",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def main() -> None: # pragma: no cover - thin argparse wrapper
|
|
252
|
+
parser = argparse.ArgumentParser(description="Authorize Classifyre against Dropbox")
|
|
253
|
+
add_dropbox_auth_arguments(parser)
|
|
254
|
+
sys.exit(run_dropbox_auth_command(parser.parse_args()))
|
|
@@ -7,7 +7,21 @@ from datetime import UTC, datetime
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
from urllib.parse import quote
|
|
9
9
|
|
|
10
|
-
from
|
|
10
|
+
# The generated model names come from each auth branch's schema `title`, which
|
|
11
|
+
# is also what the source form shows in its auth dropdown. Alias them back to
|
|
12
|
+
# stable names so renaming a label never churns this module.
|
|
13
|
+
from ...models.generated_input import (
|
|
14
|
+
DropboxAccessTokenShortLivedTestingOnly as DropboxRequiredAccessToken,
|
|
15
|
+
)
|
|
16
|
+
from ...models.generated_input import (
|
|
17
|
+
DropboxInput,
|
|
18
|
+
)
|
|
19
|
+
from ...models.generated_input import (
|
|
20
|
+
DropboxOAuthRefreshTokenRecommended as DropboxRequiredOAuth,
|
|
21
|
+
)
|
|
22
|
+
from ...models.generated_input import (
|
|
23
|
+
DropboxOAuthWithPKCENoAppSecret as DropboxRequiredOAuthPkce,
|
|
24
|
+
)
|
|
11
25
|
from ...models.generated_single_asset_scan_results import SingleAssetScanResults
|
|
12
26
|
from ..dependencies import require_module
|
|
13
27
|
from ..object_storage.base import ObjectRef, ObjectStorageSourceBase
|
|
@@ -178,6 +192,11 @@ class DropboxSource(ObjectStorageSourceBase):
|
|
|
178
192
|
Both client classes take the same credentials; what differs between an
|
|
179
193
|
individual and a Business scan is the access type granted to the app,
|
|
180
194
|
not the shape of the secret.
|
|
195
|
+
|
|
196
|
+
Refresh-token modes are the only ones that survive a scheduled run: the
|
|
197
|
+
SDK exchanges the refresh token for a new access token on every call
|
|
198
|
+
that needs one. PKCE apps refresh with the app key alone, so
|
|
199
|
+
``app_secret`` is omitted rather than empty.
|
|
181
200
|
"""
|
|
182
201
|
common: dict[str, Any] = {
|
|
183
202
|
"timeout": self._request_timeout_seconds(),
|
|
@@ -187,27 +206,64 @@ class DropboxSource(ObjectStorageSourceBase):
|
|
|
187
206
|
}
|
|
188
207
|
|
|
189
208
|
required = self.config.required
|
|
190
|
-
|
|
209
|
+
|
|
210
|
+
if isinstance(required, DropboxRequiredOAuth | DropboxRequiredOAuthPkce):
|
|
191
211
|
app_key = str(required.app_key or "").strip()
|
|
192
|
-
app_secret = self._masked_value("app_secret")
|
|
193
212
|
refresh_token = self._masked_value("refresh_token")
|
|
194
|
-
if not app_key or not
|
|
213
|
+
if not app_key or not refresh_token:
|
|
195
214
|
raise ValueError(
|
|
196
|
-
"Dropbox
|
|
197
|
-
"
|
|
215
|
+
"Dropbox refresh-token authentication requires required.app_key "
|
|
216
|
+
"and masked.refresh_token. Run "
|
|
217
|
+
"`classifyre dropbox-auth --app-key <key>` to obtain one."
|
|
198
218
|
)
|
|
199
|
-
|
|
219
|
+
credentials: dict[str, Any] = {
|
|
200
220
|
"oauth2_refresh_token": refresh_token,
|
|
201
221
|
"app_key": app_key,
|
|
202
|
-
"app_secret": app_secret,
|
|
203
222
|
**common,
|
|
204
223
|
}
|
|
224
|
+
if isinstance(required, DropboxRequiredOAuth):
|
|
225
|
+
app_secret = self._masked_value("app_secret")
|
|
226
|
+
if not app_secret:
|
|
227
|
+
raise ValueError(
|
|
228
|
+
"Dropbox OAuth authentication requires masked.app_secret. "
|
|
229
|
+
"Use auth_method 'oauth_pkce' for an app that holds no secret."
|
|
230
|
+
)
|
|
231
|
+
credentials["app_secret"] = app_secret
|
|
232
|
+
return credentials
|
|
205
233
|
|
|
206
234
|
access_token = self._masked_value("access_token")
|
|
207
235
|
if not access_token:
|
|
208
236
|
raise ValueError("Dropbox access token authentication requires masked.access_token")
|
|
209
237
|
return {"oauth2_access_token": access_token, **common}
|
|
210
238
|
|
|
239
|
+
def _uses_short_lived_token(self) -> bool:
|
|
240
|
+
return isinstance(self.config.required, DropboxRequiredAccessToken)
|
|
241
|
+
|
|
242
|
+
def _explain_auth_failure(self, exc: Exception) -> str:
|
|
243
|
+
"""Turn Dropbox's opaque auth errors into something actionable."""
|
|
244
|
+
text = str(exc)
|
|
245
|
+
if "invalid_access_token" in text and self._uses_short_lived_token():
|
|
246
|
+
return (
|
|
247
|
+
"the access token is no longer valid. App Console tokens are "
|
|
248
|
+
"short-lived and stop working a few hours after they are generated. "
|
|
249
|
+
"Switch this source to auth_method 'oauth' (or 'oauth_pkce') and run "
|
|
250
|
+
"`classifyre dropbox-auth --app-key <key> --app-secret <secret>` to "
|
|
251
|
+
"get a refresh token that does not expire."
|
|
252
|
+
)
|
|
253
|
+
if "invalid_access_token" in text:
|
|
254
|
+
return (
|
|
255
|
+
"the refresh token was rejected. It may have been revoked, or it may "
|
|
256
|
+
"belong to a different Dropbox app than the configured app key. "
|
|
257
|
+
"Re-run `classifyre dropbox-auth` to issue a new one."
|
|
258
|
+
)
|
|
259
|
+
if "missing_scope" in text:
|
|
260
|
+
return (
|
|
261
|
+
f"{text} — the refresh token was issued without a scope this scan needs. "
|
|
262
|
+
"Re-run `classifyre dropbox-auth` with the required --scope values so the "
|
|
263
|
+
"authorization covers them."
|
|
264
|
+
)
|
|
265
|
+
return text
|
|
266
|
+
|
|
211
267
|
def _build_client(self) -> Any:
|
|
212
268
|
return self._sdk().Dropbox(**self._credential_kwargs())
|
|
213
269
|
|
|
@@ -655,7 +711,7 @@ class DropboxSource(ObjectStorageSourceBase):
|
|
|
655
711
|
)
|
|
656
712
|
except Exception as exc:
|
|
657
713
|
result["status"] = "FAILURE"
|
|
658
|
-
result["message"] = f"Failed to connect to Dropbox: {exc}"
|
|
714
|
+
result["message"] = f"Failed to connect to Dropbox: {self._explain_auth_failure(exc)}"
|
|
659
715
|
return result
|
|
660
716
|
|
|
661
717
|
async def extract_raw(self) -> AsyncGenerator[list[SingleAssetScanResults], None]:
|