From 405f301b5b8a9b0c6ccbcdf74d05aced8a3694d9 Mon Sep 17 00:00:00 2001 From: DevOpsMadDog Date: Fri, 10 Oct 2025 11:49:05 +1100 Subject: [PATCH] refine stage runner registry orchestration --- WIP/README.md | 18 + .../code/backend_legacy}/__init__.py | 0 {backend => WIP/code/backend_legacy}/app.py | 0 .../code/backend_legacy}/normalizers.py | 0 .../code/backend_legacy}/pipeline.py | 0 .../code/enterprise_legacy}/.env.example | 0 .../BANK_DEPLOYMENT_GUIDE.md | 0 .../enterprise_legacy}/COMPLETE_DATA_FLOW.md | 0 .../enterprise_legacy}/COMPREHENSIVE_GUIDE.md | 0 .../code/enterprise_legacy}/Dockerfile | 0 .../enterprise_legacy}/ENHANCED_VERSION.md | 0 .../ENTERPRISE_DEPLOYMENT_GUIDE.md | 0 .../code/enterprise_legacy}/FOLDER_README.md | 0 .../PRODUCTION_DEPLOYMENT.md | 0 .../README_BANK_DEPLOYMENT.md | 0 .../enterprise_legacy}/RESEARCH_ROADMAP.md | 0 .../code/enterprise_legacy}/alembic.ini | 0 .../backstage/app-config.yaml | 0 .../backstage/catalog-info.yaml | 0 .../plugins/fixops-overview-card.tsx | 0 .../backstage/template.yaml | 0 .../terraform-deployment-template.yaml | 0 .../enterprise_legacy}/create_admin_user.py | 0 .../enterprise_legacy}/create_all_tables.py | 0 .../data/golden_regression_cases.json | 0 .../code/enterprise_legacy}/deploy-bank.sh | 0 .../enterprise_legacy}/deploy-complete.sh | 0 .../enterprise_legacy}/deploy-production.sh | 0 .../enterprise_legacy}/docker-compose.yml | 0 .../enterprise_legacy}/docs/ARCHITECTURE.md | 0 .../docs/ENTERPRISE_AUDIT_REPORT.md | 0 .../docs/FIXOPS_SUGGESTIONS_SUMMARY.md | 0 .../code/enterprise_legacy}/docs/INSTALL.md | 0 .../docs/MARKETPLACE_GUIDE.md | 0 .../enterprise_legacy}/docs/REQUIREMENTS.md | 0 .../docs/REQUIREMENTS_COMPREHENSIVE.md | 0 .../code/enterprise_legacy}/docs/ROADMAP.md | 0 .../code/enterprise_legacy}/docs/SSVC.md | 0 .../code/enterprise_legacy}/docs/index.md | 0 .../enterprise_legacy}/fixops_enterprise.db | Bin .../enterprise_legacy}/frontend/Dockerfile | 0 .../enterprise_legacy}/frontend/index.html | 0 .../enterprise_legacy}/frontend/package.json | 0 .../enterprise_legacy}/frontend/src/App.jsx | 0 .../frontend/src/components/Layout.jsx | 0 .../src/components/LoadingSpinner.jsx | 0 .../frontend/src/components/ModeToggle.jsx | 0 .../src/components/SecurityLayout.jsx | 0 .../frontend/src/components/Tooltip.jsx | 0 .../frontend/src/contexts/AuthContext.jsx | 0 .../enterprise_legacy}/frontend/src/index.css | 0 .../enterprise_legacy}/frontend/src/main.jsx | 0 .../frontend/src/pages/ArchitectDashboard.jsx | 0 .../frontend/src/pages/ArchitectureCenter.jsx | 0 .../frontend/src/pages/ArchitecturePage.jsx | 0 .../frontend/src/pages/CISODashboard.jsx | 0 .../frontend/src/pages/CommandCenter.jsx | 0 .../frontend/src/pages/DeveloperDashboard.jsx | 0 .../frontend/src/pages/DeveloperOps.jsx | 0 .../frontend/src/pages/EnhancedDashboard.jsx | 0 .../frontend/src/pages/ExecutiveBriefing.jsx | 0 .../frontend/src/pages/InstallPage.jsx | 0 .../frontend/src/utils/api.js | 0 .../frontend/tailwind.config.js | 0 .../frontend/vite.config.js | 0 .../kubernetes/backend-deployment.yaml | 0 .../kubernetes/configmap.yaml | 0 .../kubernetes/frontend-deployment.yaml | 0 .../kubernetes/ingress.yaml | 0 .../kubernetes/namespace.yaml | 0 .../enterprise_legacy}/kubernetes/pvc.yaml | 0 .../enterprise_legacy}/kubernetes/rbac.yaml | 0 .../enterprise_legacy}/kubernetes/secret.yaml | 0 .../kubernetes/services.yaml | 0 .../enterprise_legacy}/policies/sbom.rego | 0 .../policies/vulnerability.rego | 0 .../postman/FixOps-Bank-API-Collection.json | 0 ...-Bank-Development.postman_environment.json | 0 ...s-Bank-Production.postman_environment.json | 0 .../FixOps-CICD-Tests.postman_collection.json | 0 ...-Performance-Tests.postman_collection.json | 0 .../postman/POSTMAN_COMPLETION.md | 0 .../code/enterprise_legacy}/postman/README.md | 0 .../postman/sample-data/sample-sarif.json | 0 .../postman/sample-data/sample-sbom.json | 0 .../enterprise_legacy}/postman/workspace.json | 0 .../code/enterprise_legacy}/quick_start.py | 0 .../code/enterprise_legacy}/requirements.txt | 0 .../code/enterprise_legacy}/run_enterprise.py | 0 .../scripts/run_migrations.py | 0 .../scripts/run_real_cve_playbook.py | 0 .../scripts/seed_demo_data.py | 0 .../code/enterprise_legacy}/server.py | 0 .../src/api/v1/business_context.py | 0 .../src/api/v1/business_context_enhanced.py | 0 .../enterprise_legacy}/src/api/v1/cicd.py | 0 .../src/api/v1/decisions.py | 0 .../enterprise_legacy}/src/api/v1/docs.py | 0 .../enterprise_legacy}/src/api/v1/enhanced.py | 0 .../enterprise_legacy}/src/api/v1/evidence.py | 0 .../enterprise_legacy}/src/api/v1/feeds.py | 0 .../src/api/v1/marketplace.py | 0 .../src/api/v1/monitoring.py | 0 .../src/api/v1/oss_tools.py | 0 .../enterprise_legacy}/src/api/v1/policy.py | 0 .../src/api/v1/processing_layer.py | 0 .../src/api/v1/production_readiness.py | 0 .../src/api/v1/sample_data_demo.py | 0 .../enterprise_legacy}/src/api/v1/scans.py | 0 .../enterprise_legacy}/src/api/v1/system.py | 0 .../src/api/v1/system_mode.py | 0 .../code/enterprise_legacy}/src/cli/main.py | 0 .../enterprise_legacy}/src/config/settings.py | 0 .../enterprise_legacy}/src/core/exceptions.py | 0 .../enterprise_legacy}/src/core/middleware.py | 0 .../enterprise_legacy}/src/core/security.py | 0 .../src/db/migrations/__init__.py | 0 .../src/db/migrations/env.py | 0 .../migrations/versions/001_initial_schema.py | 0 .../versions/002_add_kev_waivers.py | 0 .../code/enterprise_legacy}/src/db/session.py | 0 .../code/enterprise_legacy}/src/main.py | 0 .../enterprise_legacy}/src/models/__init__.py | 0 .../enterprise_legacy}/src/models/base.py | 0 .../src/models/base_sqlite.py | 0 .../enterprise_legacy}/src/models/security.py | 0 .../src/models/security_sqlite.py | 0 .../enterprise_legacy}/src/models/user.py | 0 .../src/models/user_sqlite.py | 0 .../enterprise_legacy}/src/models/waivers.py | 0 .../enterprise_legacy}/src/schemas/user.py | 0 .../src/services/advanced_llm_engine.py | 0 .../services/business_context_processor.py | 0 .../src/services/cache_service.py | 0 .../src/services/chatgpt_client.py | 0 .../src/services/compliance_engine.py | 0 .../src/services/correlation_engine.py | 0 .../src/services/decision_engine.py | 0 .../src/services/enhanced_decision_engine.py | 0 .../src/services/evidence_export.py | 0 .../src/services/evidence_lake.py | 0 .../src/services/explainability.py | 0 .../src/services/feeds_service.py | 0 .../src/services/fix_engine.py | 0 .../src/services/golden_regression_store.py | 0 .../src/services/knowledge_graph.py | 0 .../src/services/llm_explanation_engine.py | 0 .../src/services/marketplace.py | 0 .../src/services/metrics.py | 0 .../src/services/missing_oss_integrations.py | 0 .../src/services/oss_integrations.py | 0 .../src/services/policy_engine.py | 0 .../src/services/processing_layer.py | 0 .../src/services/real_opa_engine.py | 0 .../src/services/risk_scorer.py | 0 .../src/services/rl_controller.py | 0 .../src/services/sbom_parser.py | 0 .../src/services/vector_store.py | 0 .../src/services/vex_ingestion.py | 0 .../enterprise_legacy}/src/utils/crypto.py | 0 .../enterprise_legacy}/src/utils/logger.py | 0 .../enterprise_legacy}/structlog/__init__.py | 0 .../structlog/processors.py | 0 .../enterprise_legacy}/structlog/stdlib.py | 0 .../code/enterprise_legacy}/supervisord.conf | 0 .../terraform/deployment.tf | 0 .../code/enterprise_legacy}/terraform/main.tf | 0 .../terraform/modules/backend/main.tf | 0 .../terraform/modules/mongodb/main.tf | 0 .../terraform/modules/namespace/main.tf | 0 .../enterprise_legacy}/terraform/outputs.tf | 0 .../code/enterprise_legacy}/test-bank-api.sh | 0 .../code/fastapi_legacy}/FOLDER_README.md | 0 .../code/fastapi_legacy}/__init__.py | 0 .../fastapi_legacy}/middleware/__init__.py | 0 .../code/fastapi_legacy}/middleware/cors.py | 0 .../code/fastapi_legacy}/security.py | 0 .../code/fastapi_legacy}/testclient.py | 0 .../code/perf_experiments}/BASELINE.md | 0 .../code/perf_experiments}/BENCHMARKS.csv | 0 .../code/perf_experiments}/CHANGES.md | 0 .../code/perf_experiments}/FOLDER_README.md | 0 .../code/prototype_decision_api}/__init__.py | 0 .../code/prototype_decision_api}/api.py | 0 .../processing/__init__.py | 0 .../processing/bayesian.py | 0 .../processing/explanation.py | 0 .../processing/knowledge_graph.py | 0 .../processing/sarif.py | 0 .../code/prototypes}/decider/FOLDER_README.md | 0 .../code/prototypes}/decider/__init__.py | 0 .../code/prototypes}/decider/api.py | 0 .../decider/processing/FOLDER_README.md | 0 .../decider/processing/__init__.py | 0 .../decider/processing/bayesian.py | 0 .../decider/processing/explanation.py | 0 .../decider/processing/knowledge_graph.py | 0 .../prototypes}/decider/processing/sarif.py | 0 .../scripts/run_demo_steps_legacy.py | 0 .../frontend_akido_public}/FOLDER_README.md | 0 .../ui/frontend_akido_public}/README.md | 0 .../ui/frontend_akido_public}/index.html | 0 .../frontend_akido_public}/package-lock.json | 0 .../ui/frontend_akido_public}/package.json | 0 .../ui/frontend_akido_public}/src/App.jsx | 0 .../src/components/ModeToggle.jsx | 0 .../src/components/SecurityLayout.jsx | 0 .../ui/frontend_akido_public}/src/index.css | 0 .../ui/frontend_akido_public}/src/main.jsx | 0 .../src/pages/ArchitectureCenter.jsx | 0 .../src/pages/ArchitecturePage.jsx | 0 .../src/pages/CommandCenter.jsx | 0 .../src/pages/DeveloperOps.jsx | 0 .../src/pages/ExecutiveBriefing.jsx | 0 .../src/pages/InstallPage.jsx | 0 .../ui/frontend_akido_public}/vite.config.js | 0 .../vendor/pydantic_stub}/FOLDER_README.md | 0 .../vendor/pydantic_stub}/__init__.py | 0 {torch => WIP/vendor/torch_stub}/__init__.py | 0 core/cli.py | 47 +- core/stage_runner.py | 1141 +++++++++-------- docs/FixOps_Demo_IO_Contract.md | 25 +- .../src/api/v1/artefacts.py | 10 +- .../src/config/settings.py | 92 +- .../src/core/middleware.py | 94 +- .../src/services/__init__.py | 4 +- .../src/services/feeds_service.py | 21 +- .../src/services/run_registry.py | 277 ++-- tests/test_api_artefacts.py | 85 +- tests/test_cli_stage_run.py | 146 ++- tests/test_no_wip_imports.py | 19 + 231 files changed, 1216 insertions(+), 763 deletions(-) create mode 100644 WIP/README.md rename {backend => WIP/code/backend_legacy}/__init__.py (100%) rename {backend => WIP/code/backend_legacy}/app.py (100%) rename {backend => WIP/code/backend_legacy}/normalizers.py (100%) rename {backend => WIP/code/backend_legacy}/pipeline.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/.env.example (100%) rename {enterprise => WIP/code/enterprise_legacy}/BANK_DEPLOYMENT_GUIDE.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/COMPLETE_DATA_FLOW.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/COMPREHENSIVE_GUIDE.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/Dockerfile (100%) rename {enterprise => WIP/code/enterprise_legacy}/ENHANCED_VERSION.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/ENTERPRISE_DEPLOYMENT_GUIDE.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/FOLDER_README.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/PRODUCTION_DEPLOYMENT.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/README_BANK_DEPLOYMENT.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/RESEARCH_ROADMAP.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/alembic.ini (100%) rename {enterprise => WIP/code/enterprise_legacy}/backstage/app-config.yaml (100%) rename {enterprise => WIP/code/enterprise_legacy}/backstage/catalog-info.yaml (100%) rename {enterprise => WIP/code/enterprise_legacy}/backstage/plugins/fixops-overview-card.tsx (100%) rename {enterprise => WIP/code/enterprise_legacy}/backstage/template.yaml (100%) rename {enterprise => WIP/code/enterprise_legacy}/backstage/terraform-deployment-template.yaml (100%) rename {enterprise => WIP/code/enterprise_legacy}/create_admin_user.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/create_all_tables.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/data/golden_regression_cases.json (100%) rename {enterprise => WIP/code/enterprise_legacy}/deploy-bank.sh (100%) rename {enterprise => WIP/code/enterprise_legacy}/deploy-complete.sh (100%) mode change 100755 => 100644 rename {enterprise => WIP/code/enterprise_legacy}/deploy-production.sh (100%) mode change 100755 => 100644 rename {enterprise => WIP/code/enterprise_legacy}/docker-compose.yml (100%) rename {enterprise => WIP/code/enterprise_legacy}/docs/ARCHITECTURE.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/docs/ENTERPRISE_AUDIT_REPORT.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/docs/FIXOPS_SUGGESTIONS_SUMMARY.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/docs/INSTALL.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/docs/MARKETPLACE_GUIDE.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/docs/REQUIREMENTS.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/docs/REQUIREMENTS_COMPREHENSIVE.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/docs/ROADMAP.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/docs/SSVC.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/docs/index.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/fixops_enterprise.db (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/Dockerfile (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/index.html (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/package.json (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/App.jsx (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/components/Layout.jsx (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/components/LoadingSpinner.jsx (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/components/ModeToggle.jsx (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/components/SecurityLayout.jsx (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/components/Tooltip.jsx (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/contexts/AuthContext.jsx (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/index.css (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/main.jsx (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/pages/ArchitectDashboard.jsx (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/pages/ArchitectureCenter.jsx (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/pages/ArchitecturePage.jsx (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/pages/CISODashboard.jsx (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/pages/CommandCenter.jsx (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/pages/DeveloperDashboard.jsx (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/pages/DeveloperOps.jsx (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/pages/EnhancedDashboard.jsx (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/pages/ExecutiveBriefing.jsx (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/pages/InstallPage.jsx (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/src/utils/api.js (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/tailwind.config.js (100%) rename {enterprise => WIP/code/enterprise_legacy}/frontend/vite.config.js (100%) rename {enterprise => WIP/code/enterprise_legacy}/kubernetes/backend-deployment.yaml (100%) rename {enterprise => WIP/code/enterprise_legacy}/kubernetes/configmap.yaml (100%) rename {enterprise => WIP/code/enterprise_legacy}/kubernetes/frontend-deployment.yaml (100%) rename {enterprise => WIP/code/enterprise_legacy}/kubernetes/ingress.yaml (100%) rename {enterprise => WIP/code/enterprise_legacy}/kubernetes/namespace.yaml (100%) rename {enterprise => WIP/code/enterprise_legacy}/kubernetes/pvc.yaml (100%) rename {enterprise => WIP/code/enterprise_legacy}/kubernetes/rbac.yaml (100%) rename {enterprise => WIP/code/enterprise_legacy}/kubernetes/secret.yaml (100%) rename {enterprise => WIP/code/enterprise_legacy}/kubernetes/services.yaml (100%) rename {enterprise => WIP/code/enterprise_legacy}/policies/sbom.rego (100%) rename {enterprise => WIP/code/enterprise_legacy}/policies/vulnerability.rego (100%) rename {enterprise => WIP/code/enterprise_legacy}/postman/FixOps-Bank-API-Collection.json (100%) rename {enterprise => WIP/code/enterprise_legacy}/postman/FixOps-Bank-Development.postman_environment.json (100%) rename {enterprise => WIP/code/enterprise_legacy}/postman/FixOps-Bank-Production.postman_environment.json (100%) rename {enterprise => WIP/code/enterprise_legacy}/postman/FixOps-CICD-Tests.postman_collection.json (100%) rename {enterprise => WIP/code/enterprise_legacy}/postman/FixOps-Performance-Tests.postman_collection.json (100%) rename {enterprise => WIP/code/enterprise_legacy}/postman/POSTMAN_COMPLETION.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/postman/README.md (100%) rename {enterprise => WIP/code/enterprise_legacy}/postman/sample-data/sample-sarif.json (100%) rename {enterprise => WIP/code/enterprise_legacy}/postman/sample-data/sample-sbom.json (100%) rename {enterprise => WIP/code/enterprise_legacy}/postman/workspace.json (100%) rename {enterprise => WIP/code/enterprise_legacy}/quick_start.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/requirements.txt (100%) rename {enterprise => WIP/code/enterprise_legacy}/run_enterprise.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/scripts/run_migrations.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/scripts/run_real_cve_playbook.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/scripts/seed_demo_data.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/server.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/api/v1/business_context.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/api/v1/business_context_enhanced.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/api/v1/cicd.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/api/v1/decisions.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/api/v1/docs.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/api/v1/enhanced.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/api/v1/evidence.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/api/v1/feeds.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/api/v1/marketplace.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/api/v1/monitoring.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/api/v1/oss_tools.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/api/v1/policy.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/api/v1/processing_layer.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/api/v1/production_readiness.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/api/v1/sample_data_demo.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/api/v1/scans.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/api/v1/system.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/api/v1/system_mode.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/cli/main.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/config/settings.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/core/exceptions.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/core/middleware.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/core/security.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/db/migrations/__init__.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/db/migrations/env.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/db/migrations/versions/001_initial_schema.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/db/migrations/versions/002_add_kev_waivers.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/db/session.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/main.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/models/__init__.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/models/base.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/models/base_sqlite.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/models/security.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/models/security_sqlite.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/models/user.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/models/user_sqlite.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/models/waivers.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/schemas/user.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/advanced_llm_engine.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/business_context_processor.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/cache_service.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/chatgpt_client.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/compliance_engine.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/correlation_engine.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/decision_engine.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/enhanced_decision_engine.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/evidence_export.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/evidence_lake.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/explainability.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/feeds_service.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/fix_engine.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/golden_regression_store.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/knowledge_graph.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/llm_explanation_engine.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/marketplace.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/metrics.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/missing_oss_integrations.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/oss_integrations.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/policy_engine.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/processing_layer.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/real_opa_engine.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/risk_scorer.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/rl_controller.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/sbom_parser.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/vector_store.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/services/vex_ingestion.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/utils/crypto.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/src/utils/logger.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/structlog/__init__.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/structlog/processors.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/structlog/stdlib.py (100%) rename {enterprise => WIP/code/enterprise_legacy}/supervisord.conf (100%) rename {enterprise => WIP/code/enterprise_legacy}/terraform/deployment.tf (100%) rename {enterprise => WIP/code/enterprise_legacy}/terraform/main.tf (100%) rename {enterprise => WIP/code/enterprise_legacy}/terraform/modules/backend/main.tf (100%) rename {enterprise => WIP/code/enterprise_legacy}/terraform/modules/mongodb/main.tf (100%) rename {enterprise => WIP/code/enterprise_legacy}/terraform/modules/namespace/main.tf (100%) rename {enterprise => WIP/code/enterprise_legacy}/terraform/outputs.tf (100%) rename {enterprise => WIP/code/enterprise_legacy}/test-bank-api.sh (100%) rename {fastapi => WIP/code/fastapi_legacy}/FOLDER_README.md (100%) rename {fastapi => WIP/code/fastapi_legacy}/__init__.py (100%) rename {fastapi => WIP/code/fastapi_legacy}/middleware/__init__.py (100%) rename {fastapi => WIP/code/fastapi_legacy}/middleware/cors.py (100%) rename {fastapi => WIP/code/fastapi_legacy}/security.py (100%) rename {fastapi => WIP/code/fastapi_legacy}/testclient.py (100%) rename {perf => WIP/code/perf_experiments}/BASELINE.md (100%) rename {perf => WIP/code/perf_experiments}/BENCHMARKS.csv (100%) rename {perf => WIP/code/perf_experiments}/CHANGES.md (100%) rename {perf => WIP/code/perf_experiments}/FOLDER_README.md (100%) rename {new_backend => WIP/code/prototype_decision_api}/__init__.py (100%) rename {new_backend => WIP/code/prototype_decision_api}/api.py (100%) rename {new_backend => WIP/code/prototype_decision_api}/processing/__init__.py (100%) rename {new_backend => WIP/code/prototype_decision_api}/processing/bayesian.py (100%) rename {new_backend => WIP/code/prototype_decision_api}/processing/explanation.py (100%) rename {new_backend => WIP/code/prototype_decision_api}/processing/knowledge_graph.py (100%) rename {new_backend => WIP/code/prototype_decision_api}/processing/sarif.py (100%) rename {prototypes => WIP/code/prototypes}/decider/FOLDER_README.md (100%) rename {prototypes => WIP/code/prototypes}/decider/__init__.py (100%) rename {prototypes => WIP/code/prototypes}/decider/api.py (100%) rename {prototypes => WIP/code/prototypes}/decider/processing/FOLDER_README.md (100%) rename {prototypes => WIP/code/prototypes}/decider/processing/__init__.py (100%) rename {prototypes => WIP/code/prototypes}/decider/processing/bayesian.py (100%) rename {prototypes => WIP/code/prototypes}/decider/processing/explanation.py (100%) rename {prototypes => WIP/code/prototypes}/decider/processing/knowledge_graph.py (100%) rename {prototypes => WIP/code/prototypes}/decider/processing/sarif.py (100%) rename scripts/run_demo_steps.py => WIP/scripts/run_demo_steps_legacy.py (100%) rename {frontend-akido-public => WIP/ui/frontend_akido_public}/FOLDER_README.md (100%) rename {frontend-akido-public => WIP/ui/frontend_akido_public}/README.md (100%) rename {frontend-akido-public => WIP/ui/frontend_akido_public}/index.html (100%) rename {frontend-akido-public => WIP/ui/frontend_akido_public}/package-lock.json (100%) rename {frontend-akido-public => WIP/ui/frontend_akido_public}/package.json (100%) rename {frontend-akido-public => WIP/ui/frontend_akido_public}/src/App.jsx (100%) rename {frontend-akido-public => WIP/ui/frontend_akido_public}/src/components/ModeToggle.jsx (100%) rename {frontend-akido-public => WIP/ui/frontend_akido_public}/src/components/SecurityLayout.jsx (100%) rename {frontend-akido-public => WIP/ui/frontend_akido_public}/src/index.css (100%) rename {frontend-akido-public => WIP/ui/frontend_akido_public}/src/main.jsx (100%) rename {frontend-akido-public => WIP/ui/frontend_akido_public}/src/pages/ArchitectureCenter.jsx (100%) rename {frontend-akido-public => WIP/ui/frontend_akido_public}/src/pages/ArchitecturePage.jsx (100%) rename {frontend-akido-public => WIP/ui/frontend_akido_public}/src/pages/CommandCenter.jsx (100%) rename {frontend-akido-public => WIP/ui/frontend_akido_public}/src/pages/DeveloperOps.jsx (100%) rename {frontend-akido-public => WIP/ui/frontend_akido_public}/src/pages/ExecutiveBriefing.jsx (100%) rename {frontend-akido-public => WIP/ui/frontend_akido_public}/src/pages/InstallPage.jsx (100%) rename {frontend-akido-public => WIP/ui/frontend_akido_public}/vite.config.js (100%) rename {pydantic => WIP/vendor/pydantic_stub}/FOLDER_README.md (100%) rename {pydantic => WIP/vendor/pydantic_stub}/__init__.py (100%) rename {torch => WIP/vendor/torch_stub}/__init__.py (100%) mode change 120000 => 100644 fixops-blended-enterprise/src/core/middleware.py mode change 120000 => 100644 fixops-blended-enterprise/src/services/feeds_service.py create mode 100644 tests/test_no_wip_imports.py diff --git a/WIP/README.md b/WIP/README.md new file mode 100644 index 000000000..993ea5c61 --- /dev/null +++ b/WIP/README.md @@ -0,0 +1,18 @@ +# Work-in-Progress Archive + +The `WIP/` directory quarantines legacy or experimental surfaces that are no longer part of the unified stage-run workflow but are retained for reference. The table below summarises what moved and the rationale. + +| New location | Previous path | Notes | +| --- | --- | --- | +| `WIP/code/backend_legacy/` | `backend/` | Historical Flask demo backend superseded by the canonical `core` + `apps` pipelines. | +| `WIP/code/enterprise_legacy/` | `enterprise/` | Full enterprise stack (API, DB, UI) retained for documentation but replaced by the streamlined blended services. | +| `WIP/code/fastapi_legacy/` | `fastapi/` | Early FastAPI experiments; keep isolated to avoid conflicting imports. | +| `WIP/code/perf_experiments/` | `perf/` | Performance benchmarks and notes that are not part of supported runtime paths. | +| `WIP/code/prototype_decision_api/` | `new_backend/` | Prototype decision API superseded by the new stage runner + ingest API flow. | +| `WIP/code/prototypes/` | `prototypes/` | Miscellaneous proof-of-concept pipelines; archived until individually reviewed. | +| `WIP/scripts/run_demo_steps_legacy.py` | `scripts/run_demo_steps.py` | Legacy multi-stage runner replaced by `python -m core.cli stage-run`. | +| `WIP/ui/frontend_akido_public/` | `frontend-akido-public/` | Marketing UI build not aligned with the current CLI/API demo experience. | +| `WIP/vendor/pydantic_stub/` | `pydantic/` | Local stub module for earlier experiments—kept out of import path. | +| `WIP/vendor/torch_stub/` | `torch/` | Lightweight torch placeholder used only in archived notebooks. | + +By parking these assets under `WIP/`, we avoid accidental imports (enforced by `tests/test_no_wip_imports.py`) while keeping the material available for future reference or incremental migration work. diff --git a/backend/__init__.py b/WIP/code/backend_legacy/__init__.py similarity index 100% rename from backend/__init__.py rename to WIP/code/backend_legacy/__init__.py diff --git a/backend/app.py b/WIP/code/backend_legacy/app.py similarity index 100% rename from backend/app.py rename to WIP/code/backend_legacy/app.py diff --git a/backend/normalizers.py b/WIP/code/backend_legacy/normalizers.py similarity index 100% rename from backend/normalizers.py rename to WIP/code/backend_legacy/normalizers.py diff --git a/backend/pipeline.py b/WIP/code/backend_legacy/pipeline.py similarity index 100% rename from backend/pipeline.py rename to WIP/code/backend_legacy/pipeline.py diff --git a/enterprise/.env.example b/WIP/code/enterprise_legacy/.env.example similarity index 100% rename from enterprise/.env.example rename to WIP/code/enterprise_legacy/.env.example diff --git a/enterprise/BANK_DEPLOYMENT_GUIDE.md b/WIP/code/enterprise_legacy/BANK_DEPLOYMENT_GUIDE.md similarity index 100% rename from enterprise/BANK_DEPLOYMENT_GUIDE.md rename to WIP/code/enterprise_legacy/BANK_DEPLOYMENT_GUIDE.md diff --git a/enterprise/COMPLETE_DATA_FLOW.md b/WIP/code/enterprise_legacy/COMPLETE_DATA_FLOW.md similarity index 100% rename from enterprise/COMPLETE_DATA_FLOW.md rename to WIP/code/enterprise_legacy/COMPLETE_DATA_FLOW.md diff --git a/enterprise/COMPREHENSIVE_GUIDE.md b/WIP/code/enterprise_legacy/COMPREHENSIVE_GUIDE.md similarity index 100% rename from enterprise/COMPREHENSIVE_GUIDE.md rename to WIP/code/enterprise_legacy/COMPREHENSIVE_GUIDE.md diff --git a/enterprise/Dockerfile b/WIP/code/enterprise_legacy/Dockerfile similarity index 100% rename from enterprise/Dockerfile rename to WIP/code/enterprise_legacy/Dockerfile diff --git a/enterprise/ENHANCED_VERSION.md b/WIP/code/enterprise_legacy/ENHANCED_VERSION.md similarity index 100% rename from enterprise/ENHANCED_VERSION.md rename to WIP/code/enterprise_legacy/ENHANCED_VERSION.md diff --git a/enterprise/ENTERPRISE_DEPLOYMENT_GUIDE.md b/WIP/code/enterprise_legacy/ENTERPRISE_DEPLOYMENT_GUIDE.md similarity index 100% rename from enterprise/ENTERPRISE_DEPLOYMENT_GUIDE.md rename to WIP/code/enterprise_legacy/ENTERPRISE_DEPLOYMENT_GUIDE.md diff --git a/enterprise/FOLDER_README.md b/WIP/code/enterprise_legacy/FOLDER_README.md similarity index 100% rename from enterprise/FOLDER_README.md rename to WIP/code/enterprise_legacy/FOLDER_README.md diff --git a/enterprise/PRODUCTION_DEPLOYMENT.md b/WIP/code/enterprise_legacy/PRODUCTION_DEPLOYMENT.md similarity index 100% rename from enterprise/PRODUCTION_DEPLOYMENT.md rename to WIP/code/enterprise_legacy/PRODUCTION_DEPLOYMENT.md diff --git a/enterprise/README_BANK_DEPLOYMENT.md b/WIP/code/enterprise_legacy/README_BANK_DEPLOYMENT.md similarity index 100% rename from enterprise/README_BANK_DEPLOYMENT.md rename to WIP/code/enterprise_legacy/README_BANK_DEPLOYMENT.md diff --git a/enterprise/RESEARCH_ROADMAP.md b/WIP/code/enterprise_legacy/RESEARCH_ROADMAP.md similarity index 100% rename from enterprise/RESEARCH_ROADMAP.md rename to WIP/code/enterprise_legacy/RESEARCH_ROADMAP.md diff --git a/enterprise/alembic.ini b/WIP/code/enterprise_legacy/alembic.ini similarity index 100% rename from enterprise/alembic.ini rename to WIP/code/enterprise_legacy/alembic.ini diff --git a/enterprise/backstage/app-config.yaml b/WIP/code/enterprise_legacy/backstage/app-config.yaml similarity index 100% rename from enterprise/backstage/app-config.yaml rename to WIP/code/enterprise_legacy/backstage/app-config.yaml diff --git a/enterprise/backstage/catalog-info.yaml b/WIP/code/enterprise_legacy/backstage/catalog-info.yaml similarity index 100% rename from enterprise/backstage/catalog-info.yaml rename to WIP/code/enterprise_legacy/backstage/catalog-info.yaml diff --git a/enterprise/backstage/plugins/fixops-overview-card.tsx b/WIP/code/enterprise_legacy/backstage/plugins/fixops-overview-card.tsx similarity index 100% rename from enterprise/backstage/plugins/fixops-overview-card.tsx rename to WIP/code/enterprise_legacy/backstage/plugins/fixops-overview-card.tsx diff --git a/enterprise/backstage/template.yaml b/WIP/code/enterprise_legacy/backstage/template.yaml similarity index 100% rename from enterprise/backstage/template.yaml rename to WIP/code/enterprise_legacy/backstage/template.yaml diff --git a/enterprise/backstage/terraform-deployment-template.yaml b/WIP/code/enterprise_legacy/backstage/terraform-deployment-template.yaml similarity index 100% rename from enterprise/backstage/terraform-deployment-template.yaml rename to WIP/code/enterprise_legacy/backstage/terraform-deployment-template.yaml diff --git a/enterprise/create_admin_user.py b/WIP/code/enterprise_legacy/create_admin_user.py similarity index 100% rename from enterprise/create_admin_user.py rename to WIP/code/enterprise_legacy/create_admin_user.py diff --git a/enterprise/create_all_tables.py b/WIP/code/enterprise_legacy/create_all_tables.py similarity index 100% rename from enterprise/create_all_tables.py rename to WIP/code/enterprise_legacy/create_all_tables.py diff --git a/enterprise/data/golden_regression_cases.json b/WIP/code/enterprise_legacy/data/golden_regression_cases.json similarity index 100% rename from enterprise/data/golden_regression_cases.json rename to WIP/code/enterprise_legacy/data/golden_regression_cases.json diff --git a/enterprise/deploy-bank.sh b/WIP/code/enterprise_legacy/deploy-bank.sh similarity index 100% rename from enterprise/deploy-bank.sh rename to WIP/code/enterprise_legacy/deploy-bank.sh diff --git a/enterprise/deploy-complete.sh b/WIP/code/enterprise_legacy/deploy-complete.sh old mode 100755 new mode 100644 similarity index 100% rename from enterprise/deploy-complete.sh rename to WIP/code/enterprise_legacy/deploy-complete.sh diff --git a/enterprise/deploy-production.sh b/WIP/code/enterprise_legacy/deploy-production.sh old mode 100755 new mode 100644 similarity index 100% rename from enterprise/deploy-production.sh rename to WIP/code/enterprise_legacy/deploy-production.sh diff --git a/enterprise/docker-compose.yml b/WIP/code/enterprise_legacy/docker-compose.yml similarity index 100% rename from enterprise/docker-compose.yml rename to WIP/code/enterprise_legacy/docker-compose.yml diff --git a/enterprise/docs/ARCHITECTURE.md b/WIP/code/enterprise_legacy/docs/ARCHITECTURE.md similarity index 100% rename from enterprise/docs/ARCHITECTURE.md rename to WIP/code/enterprise_legacy/docs/ARCHITECTURE.md diff --git a/enterprise/docs/ENTERPRISE_AUDIT_REPORT.md b/WIP/code/enterprise_legacy/docs/ENTERPRISE_AUDIT_REPORT.md similarity index 100% rename from enterprise/docs/ENTERPRISE_AUDIT_REPORT.md rename to WIP/code/enterprise_legacy/docs/ENTERPRISE_AUDIT_REPORT.md diff --git a/enterprise/docs/FIXOPS_SUGGESTIONS_SUMMARY.md b/WIP/code/enterprise_legacy/docs/FIXOPS_SUGGESTIONS_SUMMARY.md similarity index 100% rename from enterprise/docs/FIXOPS_SUGGESTIONS_SUMMARY.md rename to WIP/code/enterprise_legacy/docs/FIXOPS_SUGGESTIONS_SUMMARY.md diff --git a/enterprise/docs/INSTALL.md b/WIP/code/enterprise_legacy/docs/INSTALL.md similarity index 100% rename from enterprise/docs/INSTALL.md rename to WIP/code/enterprise_legacy/docs/INSTALL.md diff --git a/enterprise/docs/MARKETPLACE_GUIDE.md b/WIP/code/enterprise_legacy/docs/MARKETPLACE_GUIDE.md similarity index 100% rename from enterprise/docs/MARKETPLACE_GUIDE.md rename to WIP/code/enterprise_legacy/docs/MARKETPLACE_GUIDE.md diff --git a/enterprise/docs/REQUIREMENTS.md b/WIP/code/enterprise_legacy/docs/REQUIREMENTS.md similarity index 100% rename from enterprise/docs/REQUIREMENTS.md rename to WIP/code/enterprise_legacy/docs/REQUIREMENTS.md diff --git a/enterprise/docs/REQUIREMENTS_COMPREHENSIVE.md b/WIP/code/enterprise_legacy/docs/REQUIREMENTS_COMPREHENSIVE.md similarity index 100% rename from enterprise/docs/REQUIREMENTS_COMPREHENSIVE.md rename to WIP/code/enterprise_legacy/docs/REQUIREMENTS_COMPREHENSIVE.md diff --git a/enterprise/docs/ROADMAP.md b/WIP/code/enterprise_legacy/docs/ROADMAP.md similarity index 100% rename from enterprise/docs/ROADMAP.md rename to WIP/code/enterprise_legacy/docs/ROADMAP.md diff --git a/enterprise/docs/SSVC.md b/WIP/code/enterprise_legacy/docs/SSVC.md similarity index 100% rename from enterprise/docs/SSVC.md rename to WIP/code/enterprise_legacy/docs/SSVC.md diff --git a/enterprise/docs/index.md b/WIP/code/enterprise_legacy/docs/index.md similarity index 100% rename from enterprise/docs/index.md rename to WIP/code/enterprise_legacy/docs/index.md diff --git a/enterprise/fixops_enterprise.db b/WIP/code/enterprise_legacy/fixops_enterprise.db similarity index 100% rename from enterprise/fixops_enterprise.db rename to WIP/code/enterprise_legacy/fixops_enterprise.db diff --git a/enterprise/frontend/Dockerfile b/WIP/code/enterprise_legacy/frontend/Dockerfile similarity index 100% rename from enterprise/frontend/Dockerfile rename to WIP/code/enterprise_legacy/frontend/Dockerfile diff --git a/enterprise/frontend/index.html b/WIP/code/enterprise_legacy/frontend/index.html similarity index 100% rename from enterprise/frontend/index.html rename to WIP/code/enterprise_legacy/frontend/index.html diff --git a/enterprise/frontend/package.json b/WIP/code/enterprise_legacy/frontend/package.json similarity index 100% rename from enterprise/frontend/package.json rename to WIP/code/enterprise_legacy/frontend/package.json diff --git a/enterprise/frontend/src/App.jsx b/WIP/code/enterprise_legacy/frontend/src/App.jsx similarity index 100% rename from enterprise/frontend/src/App.jsx rename to WIP/code/enterprise_legacy/frontend/src/App.jsx diff --git a/enterprise/frontend/src/components/Layout.jsx b/WIP/code/enterprise_legacy/frontend/src/components/Layout.jsx similarity index 100% rename from enterprise/frontend/src/components/Layout.jsx rename to WIP/code/enterprise_legacy/frontend/src/components/Layout.jsx diff --git a/enterprise/frontend/src/components/LoadingSpinner.jsx b/WIP/code/enterprise_legacy/frontend/src/components/LoadingSpinner.jsx similarity index 100% rename from enterprise/frontend/src/components/LoadingSpinner.jsx rename to WIP/code/enterprise_legacy/frontend/src/components/LoadingSpinner.jsx diff --git a/enterprise/frontend/src/components/ModeToggle.jsx b/WIP/code/enterprise_legacy/frontend/src/components/ModeToggle.jsx similarity index 100% rename from enterprise/frontend/src/components/ModeToggle.jsx rename to WIP/code/enterprise_legacy/frontend/src/components/ModeToggle.jsx diff --git a/enterprise/frontend/src/components/SecurityLayout.jsx b/WIP/code/enterprise_legacy/frontend/src/components/SecurityLayout.jsx similarity index 100% rename from enterprise/frontend/src/components/SecurityLayout.jsx rename to WIP/code/enterprise_legacy/frontend/src/components/SecurityLayout.jsx diff --git a/enterprise/frontend/src/components/Tooltip.jsx b/WIP/code/enterprise_legacy/frontend/src/components/Tooltip.jsx similarity index 100% rename from enterprise/frontend/src/components/Tooltip.jsx rename to WIP/code/enterprise_legacy/frontend/src/components/Tooltip.jsx diff --git a/enterprise/frontend/src/contexts/AuthContext.jsx b/WIP/code/enterprise_legacy/frontend/src/contexts/AuthContext.jsx similarity index 100% rename from enterprise/frontend/src/contexts/AuthContext.jsx rename to WIP/code/enterprise_legacy/frontend/src/contexts/AuthContext.jsx diff --git a/enterprise/frontend/src/index.css b/WIP/code/enterprise_legacy/frontend/src/index.css similarity index 100% rename from enterprise/frontend/src/index.css rename to WIP/code/enterprise_legacy/frontend/src/index.css diff --git a/enterprise/frontend/src/main.jsx b/WIP/code/enterprise_legacy/frontend/src/main.jsx similarity index 100% rename from enterprise/frontend/src/main.jsx rename to WIP/code/enterprise_legacy/frontend/src/main.jsx diff --git a/enterprise/frontend/src/pages/ArchitectDashboard.jsx b/WIP/code/enterprise_legacy/frontend/src/pages/ArchitectDashboard.jsx similarity index 100% rename from enterprise/frontend/src/pages/ArchitectDashboard.jsx rename to WIP/code/enterprise_legacy/frontend/src/pages/ArchitectDashboard.jsx diff --git a/enterprise/frontend/src/pages/ArchitectureCenter.jsx b/WIP/code/enterprise_legacy/frontend/src/pages/ArchitectureCenter.jsx similarity index 100% rename from enterprise/frontend/src/pages/ArchitectureCenter.jsx rename to WIP/code/enterprise_legacy/frontend/src/pages/ArchitectureCenter.jsx diff --git a/enterprise/frontend/src/pages/ArchitecturePage.jsx b/WIP/code/enterprise_legacy/frontend/src/pages/ArchitecturePage.jsx similarity index 100% rename from enterprise/frontend/src/pages/ArchitecturePage.jsx rename to WIP/code/enterprise_legacy/frontend/src/pages/ArchitecturePage.jsx diff --git a/enterprise/frontend/src/pages/CISODashboard.jsx b/WIP/code/enterprise_legacy/frontend/src/pages/CISODashboard.jsx similarity index 100% rename from enterprise/frontend/src/pages/CISODashboard.jsx rename to WIP/code/enterprise_legacy/frontend/src/pages/CISODashboard.jsx diff --git a/enterprise/frontend/src/pages/CommandCenter.jsx b/WIP/code/enterprise_legacy/frontend/src/pages/CommandCenter.jsx similarity index 100% rename from enterprise/frontend/src/pages/CommandCenter.jsx rename to WIP/code/enterprise_legacy/frontend/src/pages/CommandCenter.jsx diff --git a/enterprise/frontend/src/pages/DeveloperDashboard.jsx b/WIP/code/enterprise_legacy/frontend/src/pages/DeveloperDashboard.jsx similarity index 100% rename from enterprise/frontend/src/pages/DeveloperDashboard.jsx rename to WIP/code/enterprise_legacy/frontend/src/pages/DeveloperDashboard.jsx diff --git a/enterprise/frontend/src/pages/DeveloperOps.jsx b/WIP/code/enterprise_legacy/frontend/src/pages/DeveloperOps.jsx similarity index 100% rename from enterprise/frontend/src/pages/DeveloperOps.jsx rename to WIP/code/enterprise_legacy/frontend/src/pages/DeveloperOps.jsx diff --git a/enterprise/frontend/src/pages/EnhancedDashboard.jsx b/WIP/code/enterprise_legacy/frontend/src/pages/EnhancedDashboard.jsx similarity index 100% rename from enterprise/frontend/src/pages/EnhancedDashboard.jsx rename to WIP/code/enterprise_legacy/frontend/src/pages/EnhancedDashboard.jsx diff --git a/enterprise/frontend/src/pages/ExecutiveBriefing.jsx b/WIP/code/enterprise_legacy/frontend/src/pages/ExecutiveBriefing.jsx similarity index 100% rename from enterprise/frontend/src/pages/ExecutiveBriefing.jsx rename to WIP/code/enterprise_legacy/frontend/src/pages/ExecutiveBriefing.jsx diff --git a/enterprise/frontend/src/pages/InstallPage.jsx b/WIP/code/enterprise_legacy/frontend/src/pages/InstallPage.jsx similarity index 100% rename from enterprise/frontend/src/pages/InstallPage.jsx rename to WIP/code/enterprise_legacy/frontend/src/pages/InstallPage.jsx diff --git a/enterprise/frontend/src/utils/api.js b/WIP/code/enterprise_legacy/frontend/src/utils/api.js similarity index 100% rename from enterprise/frontend/src/utils/api.js rename to WIP/code/enterprise_legacy/frontend/src/utils/api.js diff --git a/enterprise/frontend/tailwind.config.js b/WIP/code/enterprise_legacy/frontend/tailwind.config.js similarity index 100% rename from enterprise/frontend/tailwind.config.js rename to WIP/code/enterprise_legacy/frontend/tailwind.config.js diff --git a/enterprise/frontend/vite.config.js b/WIP/code/enterprise_legacy/frontend/vite.config.js similarity index 100% rename from enterprise/frontend/vite.config.js rename to WIP/code/enterprise_legacy/frontend/vite.config.js diff --git a/enterprise/kubernetes/backend-deployment.yaml b/WIP/code/enterprise_legacy/kubernetes/backend-deployment.yaml similarity index 100% rename from enterprise/kubernetes/backend-deployment.yaml rename to WIP/code/enterprise_legacy/kubernetes/backend-deployment.yaml diff --git a/enterprise/kubernetes/configmap.yaml b/WIP/code/enterprise_legacy/kubernetes/configmap.yaml similarity index 100% rename from enterprise/kubernetes/configmap.yaml rename to WIP/code/enterprise_legacy/kubernetes/configmap.yaml diff --git a/enterprise/kubernetes/frontend-deployment.yaml b/WIP/code/enterprise_legacy/kubernetes/frontend-deployment.yaml similarity index 100% rename from enterprise/kubernetes/frontend-deployment.yaml rename to WIP/code/enterprise_legacy/kubernetes/frontend-deployment.yaml diff --git a/enterprise/kubernetes/ingress.yaml b/WIP/code/enterprise_legacy/kubernetes/ingress.yaml similarity index 100% rename from enterprise/kubernetes/ingress.yaml rename to WIP/code/enterprise_legacy/kubernetes/ingress.yaml diff --git a/enterprise/kubernetes/namespace.yaml b/WIP/code/enterprise_legacy/kubernetes/namespace.yaml similarity index 100% rename from enterprise/kubernetes/namespace.yaml rename to WIP/code/enterprise_legacy/kubernetes/namespace.yaml diff --git a/enterprise/kubernetes/pvc.yaml b/WIP/code/enterprise_legacy/kubernetes/pvc.yaml similarity index 100% rename from enterprise/kubernetes/pvc.yaml rename to WIP/code/enterprise_legacy/kubernetes/pvc.yaml diff --git a/enterprise/kubernetes/rbac.yaml b/WIP/code/enterprise_legacy/kubernetes/rbac.yaml similarity index 100% rename from enterprise/kubernetes/rbac.yaml rename to WIP/code/enterprise_legacy/kubernetes/rbac.yaml diff --git a/enterprise/kubernetes/secret.yaml b/WIP/code/enterprise_legacy/kubernetes/secret.yaml similarity index 100% rename from enterprise/kubernetes/secret.yaml rename to WIP/code/enterprise_legacy/kubernetes/secret.yaml diff --git a/enterprise/kubernetes/services.yaml b/WIP/code/enterprise_legacy/kubernetes/services.yaml similarity index 100% rename from enterprise/kubernetes/services.yaml rename to WIP/code/enterprise_legacy/kubernetes/services.yaml diff --git a/enterprise/policies/sbom.rego b/WIP/code/enterprise_legacy/policies/sbom.rego similarity index 100% rename from enterprise/policies/sbom.rego rename to WIP/code/enterprise_legacy/policies/sbom.rego diff --git a/enterprise/policies/vulnerability.rego b/WIP/code/enterprise_legacy/policies/vulnerability.rego similarity index 100% rename from enterprise/policies/vulnerability.rego rename to WIP/code/enterprise_legacy/policies/vulnerability.rego diff --git a/enterprise/postman/FixOps-Bank-API-Collection.json b/WIP/code/enterprise_legacy/postman/FixOps-Bank-API-Collection.json similarity index 100% rename from enterprise/postman/FixOps-Bank-API-Collection.json rename to WIP/code/enterprise_legacy/postman/FixOps-Bank-API-Collection.json diff --git a/enterprise/postman/FixOps-Bank-Development.postman_environment.json b/WIP/code/enterprise_legacy/postman/FixOps-Bank-Development.postman_environment.json similarity index 100% rename from enterprise/postman/FixOps-Bank-Development.postman_environment.json rename to WIP/code/enterprise_legacy/postman/FixOps-Bank-Development.postman_environment.json diff --git a/enterprise/postman/FixOps-Bank-Production.postman_environment.json b/WIP/code/enterprise_legacy/postman/FixOps-Bank-Production.postman_environment.json similarity index 100% rename from enterprise/postman/FixOps-Bank-Production.postman_environment.json rename to WIP/code/enterprise_legacy/postman/FixOps-Bank-Production.postman_environment.json diff --git a/enterprise/postman/FixOps-CICD-Tests.postman_collection.json b/WIP/code/enterprise_legacy/postman/FixOps-CICD-Tests.postman_collection.json similarity index 100% rename from enterprise/postman/FixOps-CICD-Tests.postman_collection.json rename to WIP/code/enterprise_legacy/postman/FixOps-CICD-Tests.postman_collection.json diff --git a/enterprise/postman/FixOps-Performance-Tests.postman_collection.json b/WIP/code/enterprise_legacy/postman/FixOps-Performance-Tests.postman_collection.json similarity index 100% rename from enterprise/postman/FixOps-Performance-Tests.postman_collection.json rename to WIP/code/enterprise_legacy/postman/FixOps-Performance-Tests.postman_collection.json diff --git a/enterprise/postman/POSTMAN_COMPLETION.md b/WIP/code/enterprise_legacy/postman/POSTMAN_COMPLETION.md similarity index 100% rename from enterprise/postman/POSTMAN_COMPLETION.md rename to WIP/code/enterprise_legacy/postman/POSTMAN_COMPLETION.md diff --git a/enterprise/postman/README.md b/WIP/code/enterprise_legacy/postman/README.md similarity index 100% rename from enterprise/postman/README.md rename to WIP/code/enterprise_legacy/postman/README.md diff --git a/enterprise/postman/sample-data/sample-sarif.json b/WIP/code/enterprise_legacy/postman/sample-data/sample-sarif.json similarity index 100% rename from enterprise/postman/sample-data/sample-sarif.json rename to WIP/code/enterprise_legacy/postman/sample-data/sample-sarif.json diff --git a/enterprise/postman/sample-data/sample-sbom.json b/WIP/code/enterprise_legacy/postman/sample-data/sample-sbom.json similarity index 100% rename from enterprise/postman/sample-data/sample-sbom.json rename to WIP/code/enterprise_legacy/postman/sample-data/sample-sbom.json diff --git a/enterprise/postman/workspace.json b/WIP/code/enterprise_legacy/postman/workspace.json similarity index 100% rename from enterprise/postman/workspace.json rename to WIP/code/enterprise_legacy/postman/workspace.json diff --git a/enterprise/quick_start.py b/WIP/code/enterprise_legacy/quick_start.py similarity index 100% rename from enterprise/quick_start.py rename to WIP/code/enterprise_legacy/quick_start.py diff --git a/enterprise/requirements.txt b/WIP/code/enterprise_legacy/requirements.txt similarity index 100% rename from enterprise/requirements.txt rename to WIP/code/enterprise_legacy/requirements.txt diff --git a/enterprise/run_enterprise.py b/WIP/code/enterprise_legacy/run_enterprise.py similarity index 100% rename from enterprise/run_enterprise.py rename to WIP/code/enterprise_legacy/run_enterprise.py diff --git a/enterprise/scripts/run_migrations.py b/WIP/code/enterprise_legacy/scripts/run_migrations.py similarity index 100% rename from enterprise/scripts/run_migrations.py rename to WIP/code/enterprise_legacy/scripts/run_migrations.py diff --git a/enterprise/scripts/run_real_cve_playbook.py b/WIP/code/enterprise_legacy/scripts/run_real_cve_playbook.py similarity index 100% rename from enterprise/scripts/run_real_cve_playbook.py rename to WIP/code/enterprise_legacy/scripts/run_real_cve_playbook.py diff --git a/enterprise/scripts/seed_demo_data.py b/WIP/code/enterprise_legacy/scripts/seed_demo_data.py similarity index 100% rename from enterprise/scripts/seed_demo_data.py rename to WIP/code/enterprise_legacy/scripts/seed_demo_data.py diff --git a/enterprise/server.py b/WIP/code/enterprise_legacy/server.py similarity index 100% rename from enterprise/server.py rename to WIP/code/enterprise_legacy/server.py diff --git a/enterprise/src/api/v1/business_context.py b/WIP/code/enterprise_legacy/src/api/v1/business_context.py similarity index 100% rename from enterprise/src/api/v1/business_context.py rename to WIP/code/enterprise_legacy/src/api/v1/business_context.py diff --git a/enterprise/src/api/v1/business_context_enhanced.py b/WIP/code/enterprise_legacy/src/api/v1/business_context_enhanced.py similarity index 100% rename from enterprise/src/api/v1/business_context_enhanced.py rename to WIP/code/enterprise_legacy/src/api/v1/business_context_enhanced.py diff --git a/enterprise/src/api/v1/cicd.py b/WIP/code/enterprise_legacy/src/api/v1/cicd.py similarity index 100% rename from enterprise/src/api/v1/cicd.py rename to WIP/code/enterprise_legacy/src/api/v1/cicd.py diff --git a/enterprise/src/api/v1/decisions.py b/WIP/code/enterprise_legacy/src/api/v1/decisions.py similarity index 100% rename from enterprise/src/api/v1/decisions.py rename to WIP/code/enterprise_legacy/src/api/v1/decisions.py diff --git a/enterprise/src/api/v1/docs.py b/WIP/code/enterprise_legacy/src/api/v1/docs.py similarity index 100% rename from enterprise/src/api/v1/docs.py rename to WIP/code/enterprise_legacy/src/api/v1/docs.py diff --git a/enterprise/src/api/v1/enhanced.py b/WIP/code/enterprise_legacy/src/api/v1/enhanced.py similarity index 100% rename from enterprise/src/api/v1/enhanced.py rename to WIP/code/enterprise_legacy/src/api/v1/enhanced.py diff --git a/enterprise/src/api/v1/evidence.py b/WIP/code/enterprise_legacy/src/api/v1/evidence.py similarity index 100% rename from enterprise/src/api/v1/evidence.py rename to WIP/code/enterprise_legacy/src/api/v1/evidence.py diff --git a/enterprise/src/api/v1/feeds.py b/WIP/code/enterprise_legacy/src/api/v1/feeds.py similarity index 100% rename from enterprise/src/api/v1/feeds.py rename to WIP/code/enterprise_legacy/src/api/v1/feeds.py diff --git a/enterprise/src/api/v1/marketplace.py b/WIP/code/enterprise_legacy/src/api/v1/marketplace.py similarity index 100% rename from enterprise/src/api/v1/marketplace.py rename to WIP/code/enterprise_legacy/src/api/v1/marketplace.py diff --git a/enterprise/src/api/v1/monitoring.py b/WIP/code/enterprise_legacy/src/api/v1/monitoring.py similarity index 100% rename from enterprise/src/api/v1/monitoring.py rename to WIP/code/enterprise_legacy/src/api/v1/monitoring.py diff --git a/enterprise/src/api/v1/oss_tools.py b/WIP/code/enterprise_legacy/src/api/v1/oss_tools.py similarity index 100% rename from enterprise/src/api/v1/oss_tools.py rename to WIP/code/enterprise_legacy/src/api/v1/oss_tools.py diff --git a/enterprise/src/api/v1/policy.py b/WIP/code/enterprise_legacy/src/api/v1/policy.py similarity index 100% rename from enterprise/src/api/v1/policy.py rename to WIP/code/enterprise_legacy/src/api/v1/policy.py diff --git a/enterprise/src/api/v1/processing_layer.py b/WIP/code/enterprise_legacy/src/api/v1/processing_layer.py similarity index 100% rename from enterprise/src/api/v1/processing_layer.py rename to WIP/code/enterprise_legacy/src/api/v1/processing_layer.py diff --git a/enterprise/src/api/v1/production_readiness.py b/WIP/code/enterprise_legacy/src/api/v1/production_readiness.py similarity index 100% rename from enterprise/src/api/v1/production_readiness.py rename to WIP/code/enterprise_legacy/src/api/v1/production_readiness.py diff --git a/enterprise/src/api/v1/sample_data_demo.py b/WIP/code/enterprise_legacy/src/api/v1/sample_data_demo.py similarity index 100% rename from enterprise/src/api/v1/sample_data_demo.py rename to WIP/code/enterprise_legacy/src/api/v1/sample_data_demo.py diff --git a/enterprise/src/api/v1/scans.py b/WIP/code/enterprise_legacy/src/api/v1/scans.py similarity index 100% rename from enterprise/src/api/v1/scans.py rename to WIP/code/enterprise_legacy/src/api/v1/scans.py diff --git a/enterprise/src/api/v1/system.py b/WIP/code/enterprise_legacy/src/api/v1/system.py similarity index 100% rename from enterprise/src/api/v1/system.py rename to WIP/code/enterprise_legacy/src/api/v1/system.py diff --git a/enterprise/src/api/v1/system_mode.py b/WIP/code/enterprise_legacy/src/api/v1/system_mode.py similarity index 100% rename from enterprise/src/api/v1/system_mode.py rename to WIP/code/enterprise_legacy/src/api/v1/system_mode.py diff --git a/enterprise/src/cli/main.py b/WIP/code/enterprise_legacy/src/cli/main.py similarity index 100% rename from enterprise/src/cli/main.py rename to WIP/code/enterprise_legacy/src/cli/main.py diff --git a/enterprise/src/config/settings.py b/WIP/code/enterprise_legacy/src/config/settings.py similarity index 100% rename from enterprise/src/config/settings.py rename to WIP/code/enterprise_legacy/src/config/settings.py diff --git a/enterprise/src/core/exceptions.py b/WIP/code/enterprise_legacy/src/core/exceptions.py similarity index 100% rename from enterprise/src/core/exceptions.py rename to WIP/code/enterprise_legacy/src/core/exceptions.py diff --git a/enterprise/src/core/middleware.py b/WIP/code/enterprise_legacy/src/core/middleware.py similarity index 100% rename from enterprise/src/core/middleware.py rename to WIP/code/enterprise_legacy/src/core/middleware.py diff --git a/enterprise/src/core/security.py b/WIP/code/enterprise_legacy/src/core/security.py similarity index 100% rename from enterprise/src/core/security.py rename to WIP/code/enterprise_legacy/src/core/security.py diff --git a/enterprise/src/db/migrations/__init__.py b/WIP/code/enterprise_legacy/src/db/migrations/__init__.py similarity index 100% rename from enterprise/src/db/migrations/__init__.py rename to WIP/code/enterprise_legacy/src/db/migrations/__init__.py diff --git a/enterprise/src/db/migrations/env.py b/WIP/code/enterprise_legacy/src/db/migrations/env.py similarity index 100% rename from enterprise/src/db/migrations/env.py rename to WIP/code/enterprise_legacy/src/db/migrations/env.py diff --git a/enterprise/src/db/migrations/versions/001_initial_schema.py b/WIP/code/enterprise_legacy/src/db/migrations/versions/001_initial_schema.py similarity index 100% rename from enterprise/src/db/migrations/versions/001_initial_schema.py rename to WIP/code/enterprise_legacy/src/db/migrations/versions/001_initial_schema.py diff --git a/enterprise/src/db/migrations/versions/002_add_kev_waivers.py b/WIP/code/enterprise_legacy/src/db/migrations/versions/002_add_kev_waivers.py similarity index 100% rename from enterprise/src/db/migrations/versions/002_add_kev_waivers.py rename to WIP/code/enterprise_legacy/src/db/migrations/versions/002_add_kev_waivers.py diff --git a/enterprise/src/db/session.py b/WIP/code/enterprise_legacy/src/db/session.py similarity index 100% rename from enterprise/src/db/session.py rename to WIP/code/enterprise_legacy/src/db/session.py diff --git a/enterprise/src/main.py b/WIP/code/enterprise_legacy/src/main.py similarity index 100% rename from enterprise/src/main.py rename to WIP/code/enterprise_legacy/src/main.py diff --git a/enterprise/src/models/__init__.py b/WIP/code/enterprise_legacy/src/models/__init__.py similarity index 100% rename from enterprise/src/models/__init__.py rename to WIP/code/enterprise_legacy/src/models/__init__.py diff --git a/enterprise/src/models/base.py b/WIP/code/enterprise_legacy/src/models/base.py similarity index 100% rename from enterprise/src/models/base.py rename to WIP/code/enterprise_legacy/src/models/base.py diff --git a/enterprise/src/models/base_sqlite.py b/WIP/code/enterprise_legacy/src/models/base_sqlite.py similarity index 100% rename from enterprise/src/models/base_sqlite.py rename to WIP/code/enterprise_legacy/src/models/base_sqlite.py diff --git a/enterprise/src/models/security.py b/WIP/code/enterprise_legacy/src/models/security.py similarity index 100% rename from enterprise/src/models/security.py rename to WIP/code/enterprise_legacy/src/models/security.py diff --git a/enterprise/src/models/security_sqlite.py b/WIP/code/enterprise_legacy/src/models/security_sqlite.py similarity index 100% rename from enterprise/src/models/security_sqlite.py rename to WIP/code/enterprise_legacy/src/models/security_sqlite.py diff --git a/enterprise/src/models/user.py b/WIP/code/enterprise_legacy/src/models/user.py similarity index 100% rename from enterprise/src/models/user.py rename to WIP/code/enterprise_legacy/src/models/user.py diff --git a/enterprise/src/models/user_sqlite.py b/WIP/code/enterprise_legacy/src/models/user_sqlite.py similarity index 100% rename from enterprise/src/models/user_sqlite.py rename to WIP/code/enterprise_legacy/src/models/user_sqlite.py diff --git a/enterprise/src/models/waivers.py b/WIP/code/enterprise_legacy/src/models/waivers.py similarity index 100% rename from enterprise/src/models/waivers.py rename to WIP/code/enterprise_legacy/src/models/waivers.py diff --git a/enterprise/src/schemas/user.py b/WIP/code/enterprise_legacy/src/schemas/user.py similarity index 100% rename from enterprise/src/schemas/user.py rename to WIP/code/enterprise_legacy/src/schemas/user.py diff --git a/enterprise/src/services/advanced_llm_engine.py b/WIP/code/enterprise_legacy/src/services/advanced_llm_engine.py similarity index 100% rename from enterprise/src/services/advanced_llm_engine.py rename to WIP/code/enterprise_legacy/src/services/advanced_llm_engine.py diff --git a/enterprise/src/services/business_context_processor.py b/WIP/code/enterprise_legacy/src/services/business_context_processor.py similarity index 100% rename from enterprise/src/services/business_context_processor.py rename to WIP/code/enterprise_legacy/src/services/business_context_processor.py diff --git a/enterprise/src/services/cache_service.py b/WIP/code/enterprise_legacy/src/services/cache_service.py similarity index 100% rename from enterprise/src/services/cache_service.py rename to WIP/code/enterprise_legacy/src/services/cache_service.py diff --git a/enterprise/src/services/chatgpt_client.py b/WIP/code/enterprise_legacy/src/services/chatgpt_client.py similarity index 100% rename from enterprise/src/services/chatgpt_client.py rename to WIP/code/enterprise_legacy/src/services/chatgpt_client.py diff --git a/enterprise/src/services/compliance_engine.py b/WIP/code/enterprise_legacy/src/services/compliance_engine.py similarity index 100% rename from enterprise/src/services/compliance_engine.py rename to WIP/code/enterprise_legacy/src/services/compliance_engine.py diff --git a/enterprise/src/services/correlation_engine.py b/WIP/code/enterprise_legacy/src/services/correlation_engine.py similarity index 100% rename from enterprise/src/services/correlation_engine.py rename to WIP/code/enterprise_legacy/src/services/correlation_engine.py diff --git a/enterprise/src/services/decision_engine.py b/WIP/code/enterprise_legacy/src/services/decision_engine.py similarity index 100% rename from enterprise/src/services/decision_engine.py rename to WIP/code/enterprise_legacy/src/services/decision_engine.py diff --git a/enterprise/src/services/enhanced_decision_engine.py b/WIP/code/enterprise_legacy/src/services/enhanced_decision_engine.py similarity index 100% rename from enterprise/src/services/enhanced_decision_engine.py rename to WIP/code/enterprise_legacy/src/services/enhanced_decision_engine.py diff --git a/enterprise/src/services/evidence_export.py b/WIP/code/enterprise_legacy/src/services/evidence_export.py similarity index 100% rename from enterprise/src/services/evidence_export.py rename to WIP/code/enterprise_legacy/src/services/evidence_export.py diff --git a/enterprise/src/services/evidence_lake.py b/WIP/code/enterprise_legacy/src/services/evidence_lake.py similarity index 100% rename from enterprise/src/services/evidence_lake.py rename to WIP/code/enterprise_legacy/src/services/evidence_lake.py diff --git a/enterprise/src/services/explainability.py b/WIP/code/enterprise_legacy/src/services/explainability.py similarity index 100% rename from enterprise/src/services/explainability.py rename to WIP/code/enterprise_legacy/src/services/explainability.py diff --git a/enterprise/src/services/feeds_service.py b/WIP/code/enterprise_legacy/src/services/feeds_service.py similarity index 100% rename from enterprise/src/services/feeds_service.py rename to WIP/code/enterprise_legacy/src/services/feeds_service.py diff --git a/enterprise/src/services/fix_engine.py b/WIP/code/enterprise_legacy/src/services/fix_engine.py similarity index 100% rename from enterprise/src/services/fix_engine.py rename to WIP/code/enterprise_legacy/src/services/fix_engine.py diff --git a/enterprise/src/services/golden_regression_store.py b/WIP/code/enterprise_legacy/src/services/golden_regression_store.py similarity index 100% rename from enterprise/src/services/golden_regression_store.py rename to WIP/code/enterprise_legacy/src/services/golden_regression_store.py diff --git a/enterprise/src/services/knowledge_graph.py b/WIP/code/enterprise_legacy/src/services/knowledge_graph.py similarity index 100% rename from enterprise/src/services/knowledge_graph.py rename to WIP/code/enterprise_legacy/src/services/knowledge_graph.py diff --git a/enterprise/src/services/llm_explanation_engine.py b/WIP/code/enterprise_legacy/src/services/llm_explanation_engine.py similarity index 100% rename from enterprise/src/services/llm_explanation_engine.py rename to WIP/code/enterprise_legacy/src/services/llm_explanation_engine.py diff --git a/enterprise/src/services/marketplace.py b/WIP/code/enterprise_legacy/src/services/marketplace.py similarity index 100% rename from enterprise/src/services/marketplace.py rename to WIP/code/enterprise_legacy/src/services/marketplace.py diff --git a/enterprise/src/services/metrics.py b/WIP/code/enterprise_legacy/src/services/metrics.py similarity index 100% rename from enterprise/src/services/metrics.py rename to WIP/code/enterprise_legacy/src/services/metrics.py diff --git a/enterprise/src/services/missing_oss_integrations.py b/WIP/code/enterprise_legacy/src/services/missing_oss_integrations.py similarity index 100% rename from enterprise/src/services/missing_oss_integrations.py rename to WIP/code/enterprise_legacy/src/services/missing_oss_integrations.py diff --git a/enterprise/src/services/oss_integrations.py b/WIP/code/enterprise_legacy/src/services/oss_integrations.py similarity index 100% rename from enterprise/src/services/oss_integrations.py rename to WIP/code/enterprise_legacy/src/services/oss_integrations.py diff --git a/enterprise/src/services/policy_engine.py b/WIP/code/enterprise_legacy/src/services/policy_engine.py similarity index 100% rename from enterprise/src/services/policy_engine.py rename to WIP/code/enterprise_legacy/src/services/policy_engine.py diff --git a/enterprise/src/services/processing_layer.py b/WIP/code/enterprise_legacy/src/services/processing_layer.py similarity index 100% rename from enterprise/src/services/processing_layer.py rename to WIP/code/enterprise_legacy/src/services/processing_layer.py diff --git a/enterprise/src/services/real_opa_engine.py b/WIP/code/enterprise_legacy/src/services/real_opa_engine.py similarity index 100% rename from enterprise/src/services/real_opa_engine.py rename to WIP/code/enterprise_legacy/src/services/real_opa_engine.py diff --git a/enterprise/src/services/risk_scorer.py b/WIP/code/enterprise_legacy/src/services/risk_scorer.py similarity index 100% rename from enterprise/src/services/risk_scorer.py rename to WIP/code/enterprise_legacy/src/services/risk_scorer.py diff --git a/enterprise/src/services/rl_controller.py b/WIP/code/enterprise_legacy/src/services/rl_controller.py similarity index 100% rename from enterprise/src/services/rl_controller.py rename to WIP/code/enterprise_legacy/src/services/rl_controller.py diff --git a/enterprise/src/services/sbom_parser.py b/WIP/code/enterprise_legacy/src/services/sbom_parser.py similarity index 100% rename from enterprise/src/services/sbom_parser.py rename to WIP/code/enterprise_legacy/src/services/sbom_parser.py diff --git a/enterprise/src/services/vector_store.py b/WIP/code/enterprise_legacy/src/services/vector_store.py similarity index 100% rename from enterprise/src/services/vector_store.py rename to WIP/code/enterprise_legacy/src/services/vector_store.py diff --git a/enterprise/src/services/vex_ingestion.py b/WIP/code/enterprise_legacy/src/services/vex_ingestion.py similarity index 100% rename from enterprise/src/services/vex_ingestion.py rename to WIP/code/enterprise_legacy/src/services/vex_ingestion.py diff --git a/enterprise/src/utils/crypto.py b/WIP/code/enterprise_legacy/src/utils/crypto.py similarity index 100% rename from enterprise/src/utils/crypto.py rename to WIP/code/enterprise_legacy/src/utils/crypto.py diff --git a/enterprise/src/utils/logger.py b/WIP/code/enterprise_legacy/src/utils/logger.py similarity index 100% rename from enterprise/src/utils/logger.py rename to WIP/code/enterprise_legacy/src/utils/logger.py diff --git a/enterprise/structlog/__init__.py b/WIP/code/enterprise_legacy/structlog/__init__.py similarity index 100% rename from enterprise/structlog/__init__.py rename to WIP/code/enterprise_legacy/structlog/__init__.py diff --git a/enterprise/structlog/processors.py b/WIP/code/enterprise_legacy/structlog/processors.py similarity index 100% rename from enterprise/structlog/processors.py rename to WIP/code/enterprise_legacy/structlog/processors.py diff --git a/enterprise/structlog/stdlib.py b/WIP/code/enterprise_legacy/structlog/stdlib.py similarity index 100% rename from enterprise/structlog/stdlib.py rename to WIP/code/enterprise_legacy/structlog/stdlib.py diff --git a/enterprise/supervisord.conf b/WIP/code/enterprise_legacy/supervisord.conf similarity index 100% rename from enterprise/supervisord.conf rename to WIP/code/enterprise_legacy/supervisord.conf diff --git a/enterprise/terraform/deployment.tf b/WIP/code/enterprise_legacy/terraform/deployment.tf similarity index 100% rename from enterprise/terraform/deployment.tf rename to WIP/code/enterprise_legacy/terraform/deployment.tf diff --git a/enterprise/terraform/main.tf b/WIP/code/enterprise_legacy/terraform/main.tf similarity index 100% rename from enterprise/terraform/main.tf rename to WIP/code/enterprise_legacy/terraform/main.tf diff --git a/enterprise/terraform/modules/backend/main.tf b/WIP/code/enterprise_legacy/terraform/modules/backend/main.tf similarity index 100% rename from enterprise/terraform/modules/backend/main.tf rename to WIP/code/enterprise_legacy/terraform/modules/backend/main.tf diff --git a/enterprise/terraform/modules/mongodb/main.tf b/WIP/code/enterprise_legacy/terraform/modules/mongodb/main.tf similarity index 100% rename from enterprise/terraform/modules/mongodb/main.tf rename to WIP/code/enterprise_legacy/terraform/modules/mongodb/main.tf diff --git a/enterprise/terraform/modules/namespace/main.tf b/WIP/code/enterprise_legacy/terraform/modules/namespace/main.tf similarity index 100% rename from enterprise/terraform/modules/namespace/main.tf rename to WIP/code/enterprise_legacy/terraform/modules/namespace/main.tf diff --git a/enterprise/terraform/outputs.tf b/WIP/code/enterprise_legacy/terraform/outputs.tf similarity index 100% rename from enterprise/terraform/outputs.tf rename to WIP/code/enterprise_legacy/terraform/outputs.tf diff --git a/enterprise/test-bank-api.sh b/WIP/code/enterprise_legacy/test-bank-api.sh similarity index 100% rename from enterprise/test-bank-api.sh rename to WIP/code/enterprise_legacy/test-bank-api.sh diff --git a/fastapi/FOLDER_README.md b/WIP/code/fastapi_legacy/FOLDER_README.md similarity index 100% rename from fastapi/FOLDER_README.md rename to WIP/code/fastapi_legacy/FOLDER_README.md diff --git a/fastapi/__init__.py b/WIP/code/fastapi_legacy/__init__.py similarity index 100% rename from fastapi/__init__.py rename to WIP/code/fastapi_legacy/__init__.py diff --git a/fastapi/middleware/__init__.py b/WIP/code/fastapi_legacy/middleware/__init__.py similarity index 100% rename from fastapi/middleware/__init__.py rename to WIP/code/fastapi_legacy/middleware/__init__.py diff --git a/fastapi/middleware/cors.py b/WIP/code/fastapi_legacy/middleware/cors.py similarity index 100% rename from fastapi/middleware/cors.py rename to WIP/code/fastapi_legacy/middleware/cors.py diff --git a/fastapi/security.py b/WIP/code/fastapi_legacy/security.py similarity index 100% rename from fastapi/security.py rename to WIP/code/fastapi_legacy/security.py diff --git a/fastapi/testclient.py b/WIP/code/fastapi_legacy/testclient.py similarity index 100% rename from fastapi/testclient.py rename to WIP/code/fastapi_legacy/testclient.py diff --git a/perf/BASELINE.md b/WIP/code/perf_experiments/BASELINE.md similarity index 100% rename from perf/BASELINE.md rename to WIP/code/perf_experiments/BASELINE.md diff --git a/perf/BENCHMARKS.csv b/WIP/code/perf_experiments/BENCHMARKS.csv similarity index 100% rename from perf/BENCHMARKS.csv rename to WIP/code/perf_experiments/BENCHMARKS.csv diff --git a/perf/CHANGES.md b/WIP/code/perf_experiments/CHANGES.md similarity index 100% rename from perf/CHANGES.md rename to WIP/code/perf_experiments/CHANGES.md diff --git a/perf/FOLDER_README.md b/WIP/code/perf_experiments/FOLDER_README.md similarity index 100% rename from perf/FOLDER_README.md rename to WIP/code/perf_experiments/FOLDER_README.md diff --git a/new_backend/__init__.py b/WIP/code/prototype_decision_api/__init__.py similarity index 100% rename from new_backend/__init__.py rename to WIP/code/prototype_decision_api/__init__.py diff --git a/new_backend/api.py b/WIP/code/prototype_decision_api/api.py similarity index 100% rename from new_backend/api.py rename to WIP/code/prototype_decision_api/api.py diff --git a/new_backend/processing/__init__.py b/WIP/code/prototype_decision_api/processing/__init__.py similarity index 100% rename from new_backend/processing/__init__.py rename to WIP/code/prototype_decision_api/processing/__init__.py diff --git a/new_backend/processing/bayesian.py b/WIP/code/prototype_decision_api/processing/bayesian.py similarity index 100% rename from new_backend/processing/bayesian.py rename to WIP/code/prototype_decision_api/processing/bayesian.py diff --git a/new_backend/processing/explanation.py b/WIP/code/prototype_decision_api/processing/explanation.py similarity index 100% rename from new_backend/processing/explanation.py rename to WIP/code/prototype_decision_api/processing/explanation.py diff --git a/new_backend/processing/knowledge_graph.py b/WIP/code/prototype_decision_api/processing/knowledge_graph.py similarity index 100% rename from new_backend/processing/knowledge_graph.py rename to WIP/code/prototype_decision_api/processing/knowledge_graph.py diff --git a/new_backend/processing/sarif.py b/WIP/code/prototype_decision_api/processing/sarif.py similarity index 100% rename from new_backend/processing/sarif.py rename to WIP/code/prototype_decision_api/processing/sarif.py diff --git a/prototypes/decider/FOLDER_README.md b/WIP/code/prototypes/decider/FOLDER_README.md similarity index 100% rename from prototypes/decider/FOLDER_README.md rename to WIP/code/prototypes/decider/FOLDER_README.md diff --git a/prototypes/decider/__init__.py b/WIP/code/prototypes/decider/__init__.py similarity index 100% rename from prototypes/decider/__init__.py rename to WIP/code/prototypes/decider/__init__.py diff --git a/prototypes/decider/api.py b/WIP/code/prototypes/decider/api.py similarity index 100% rename from prototypes/decider/api.py rename to WIP/code/prototypes/decider/api.py diff --git a/prototypes/decider/processing/FOLDER_README.md b/WIP/code/prototypes/decider/processing/FOLDER_README.md similarity index 100% rename from prototypes/decider/processing/FOLDER_README.md rename to WIP/code/prototypes/decider/processing/FOLDER_README.md diff --git a/prototypes/decider/processing/__init__.py b/WIP/code/prototypes/decider/processing/__init__.py similarity index 100% rename from prototypes/decider/processing/__init__.py rename to WIP/code/prototypes/decider/processing/__init__.py diff --git a/prototypes/decider/processing/bayesian.py b/WIP/code/prototypes/decider/processing/bayesian.py similarity index 100% rename from prototypes/decider/processing/bayesian.py rename to WIP/code/prototypes/decider/processing/bayesian.py diff --git a/prototypes/decider/processing/explanation.py b/WIP/code/prototypes/decider/processing/explanation.py similarity index 100% rename from prototypes/decider/processing/explanation.py rename to WIP/code/prototypes/decider/processing/explanation.py diff --git a/prototypes/decider/processing/knowledge_graph.py b/WIP/code/prototypes/decider/processing/knowledge_graph.py similarity index 100% rename from prototypes/decider/processing/knowledge_graph.py rename to WIP/code/prototypes/decider/processing/knowledge_graph.py diff --git a/prototypes/decider/processing/sarif.py b/WIP/code/prototypes/decider/processing/sarif.py similarity index 100% rename from prototypes/decider/processing/sarif.py rename to WIP/code/prototypes/decider/processing/sarif.py diff --git a/scripts/run_demo_steps.py b/WIP/scripts/run_demo_steps_legacy.py similarity index 100% rename from scripts/run_demo_steps.py rename to WIP/scripts/run_demo_steps_legacy.py diff --git a/frontend-akido-public/FOLDER_README.md b/WIP/ui/frontend_akido_public/FOLDER_README.md similarity index 100% rename from frontend-akido-public/FOLDER_README.md rename to WIP/ui/frontend_akido_public/FOLDER_README.md diff --git a/frontend-akido-public/README.md b/WIP/ui/frontend_akido_public/README.md similarity index 100% rename from frontend-akido-public/README.md rename to WIP/ui/frontend_akido_public/README.md diff --git a/frontend-akido-public/index.html b/WIP/ui/frontend_akido_public/index.html similarity index 100% rename from frontend-akido-public/index.html rename to WIP/ui/frontend_akido_public/index.html diff --git a/frontend-akido-public/package-lock.json b/WIP/ui/frontend_akido_public/package-lock.json similarity index 100% rename from frontend-akido-public/package-lock.json rename to WIP/ui/frontend_akido_public/package-lock.json diff --git a/frontend-akido-public/package.json b/WIP/ui/frontend_akido_public/package.json similarity index 100% rename from frontend-akido-public/package.json rename to WIP/ui/frontend_akido_public/package.json diff --git a/frontend-akido-public/src/App.jsx b/WIP/ui/frontend_akido_public/src/App.jsx similarity index 100% rename from frontend-akido-public/src/App.jsx rename to WIP/ui/frontend_akido_public/src/App.jsx diff --git a/frontend-akido-public/src/components/ModeToggle.jsx b/WIP/ui/frontend_akido_public/src/components/ModeToggle.jsx similarity index 100% rename from frontend-akido-public/src/components/ModeToggle.jsx rename to WIP/ui/frontend_akido_public/src/components/ModeToggle.jsx diff --git a/frontend-akido-public/src/components/SecurityLayout.jsx b/WIP/ui/frontend_akido_public/src/components/SecurityLayout.jsx similarity index 100% rename from frontend-akido-public/src/components/SecurityLayout.jsx rename to WIP/ui/frontend_akido_public/src/components/SecurityLayout.jsx diff --git a/frontend-akido-public/src/index.css b/WIP/ui/frontend_akido_public/src/index.css similarity index 100% rename from frontend-akido-public/src/index.css rename to WIP/ui/frontend_akido_public/src/index.css diff --git a/frontend-akido-public/src/main.jsx b/WIP/ui/frontend_akido_public/src/main.jsx similarity index 100% rename from frontend-akido-public/src/main.jsx rename to WIP/ui/frontend_akido_public/src/main.jsx diff --git a/frontend-akido-public/src/pages/ArchitectureCenter.jsx b/WIP/ui/frontend_akido_public/src/pages/ArchitectureCenter.jsx similarity index 100% rename from frontend-akido-public/src/pages/ArchitectureCenter.jsx rename to WIP/ui/frontend_akido_public/src/pages/ArchitectureCenter.jsx diff --git a/frontend-akido-public/src/pages/ArchitecturePage.jsx b/WIP/ui/frontend_akido_public/src/pages/ArchitecturePage.jsx similarity index 100% rename from frontend-akido-public/src/pages/ArchitecturePage.jsx rename to WIP/ui/frontend_akido_public/src/pages/ArchitecturePage.jsx diff --git a/frontend-akido-public/src/pages/CommandCenter.jsx b/WIP/ui/frontend_akido_public/src/pages/CommandCenter.jsx similarity index 100% rename from frontend-akido-public/src/pages/CommandCenter.jsx rename to WIP/ui/frontend_akido_public/src/pages/CommandCenter.jsx diff --git a/frontend-akido-public/src/pages/DeveloperOps.jsx b/WIP/ui/frontend_akido_public/src/pages/DeveloperOps.jsx similarity index 100% rename from frontend-akido-public/src/pages/DeveloperOps.jsx rename to WIP/ui/frontend_akido_public/src/pages/DeveloperOps.jsx diff --git a/frontend-akido-public/src/pages/ExecutiveBriefing.jsx b/WIP/ui/frontend_akido_public/src/pages/ExecutiveBriefing.jsx similarity index 100% rename from frontend-akido-public/src/pages/ExecutiveBriefing.jsx rename to WIP/ui/frontend_akido_public/src/pages/ExecutiveBriefing.jsx diff --git a/frontend-akido-public/src/pages/InstallPage.jsx b/WIP/ui/frontend_akido_public/src/pages/InstallPage.jsx similarity index 100% rename from frontend-akido-public/src/pages/InstallPage.jsx rename to WIP/ui/frontend_akido_public/src/pages/InstallPage.jsx diff --git a/frontend-akido-public/vite.config.js b/WIP/ui/frontend_akido_public/vite.config.js similarity index 100% rename from frontend-akido-public/vite.config.js rename to WIP/ui/frontend_akido_public/vite.config.js diff --git a/pydantic/FOLDER_README.md b/WIP/vendor/pydantic_stub/FOLDER_README.md similarity index 100% rename from pydantic/FOLDER_README.md rename to WIP/vendor/pydantic_stub/FOLDER_README.md diff --git a/pydantic/__init__.py b/WIP/vendor/pydantic_stub/__init__.py similarity index 100% rename from pydantic/__init__.py rename to WIP/vendor/pydantic_stub/__init__.py diff --git a/torch/__init__.py b/WIP/vendor/torch_stub/__init__.py similarity index 100% rename from torch/__init__.py rename to WIP/vendor/torch_stub/__init__.py diff --git a/core/cli.py b/core/cli.py index 54e7081c0..af073c73d 100644 --- a/core/cli.py +++ b/core/cli.py @@ -7,6 +7,12 @@ import os import sys from pathlib import Path + +ENTERPRISE_SRC = Path(__file__).resolve().parent.parent / "fixops-blended-enterprise" +if ENTERPRISE_SRC.exists(): + enterprise_path = str(ENTERPRISE_SRC) + if enterprise_path not in sys.path: + sys.path.insert(0, enterprise_path) from typing import Any, Dict, Iterable, Mapping, Optional, Sequence from apps.api.normalizers import InputNormalizer, NormalizedCVEFeed, NormalizedSARIF, NormalizedSBOM @@ -17,6 +23,8 @@ from core.storage import ArtefactArchive from core.probabilistic import ProbabilisticForecastEngine from core.stage_runner import StageRunner +from src.services.run_registry import RunRegistry +from src.services import id_allocator, signing def _apply_env_overrides(pairs: Iterable[str]) -> None: @@ -167,14 +175,17 @@ def _handle_stage_run(args: argparse.Namespace) -> int: output_path = output_path.expanduser().resolve() if args.sign and not (os.environ.get("FIXOPS_SIGNING_KEY") and os.environ.get("FIXOPS_SIGNING_KID")): - print("Signing requested but FIXOPS_SIGNING_KEY/FIXOPS_SIGNING_KID not set; proceeding without signatures.") + print( + "Signing requested but FIXOPS_SIGNING_KEY/FIXOPS_SIGNING_KID not set; proceeding without signatures." + ) - runner = StageRunner() - result = runner.run_stage( + registry = RunRegistry() + runner = StageRunner(registry, id_allocator, signing) + summary = runner.run_stage( args.stage, input_path, app_name=args.app, - app_id=args.app, + app_id=None, output_path=output_path, mode=args.mode, sign=args.sign, @@ -182,17 +193,25 @@ def _handle_stage_run(args: argparse.Namespace) -> int: verbose=args.verbose, ) - print(f"Stage '{result.stage}' materialised for app {result.app_id} run {result.run_id}.") - print(f" Output file: {result.output_file}") + try: + output_relative = summary.output_file.relative_to(Path.cwd()) + except ValueError: + output_relative = summary.output_file + print(f"✅ Stage {summary.stage} complete → wrote {output_relative}") + print(f" app_id={summary.app_id} run_id={summary.run_id}") if output_path is not None: - print(f" Copied output to: {output_path}") - print(f" Run outputs directory: {result.outputs_dir}") - if result.signed: - print(f" Signed manifests: {[path.name for path in result.signed]}") - if result.transparency_index: - print(f" Transparency log: {result.transparency_index}") - if result.bundle: - print(f" Evidence bundle: {result.bundle}") + print(f" Copied output to: {output_path}") + if summary.signatures: + joined = ", ".join(path.name for path in summary.signatures) + print(f" Signed manifests: {joined}") + if summary.transparency_index: + print(f" Transparency index: {summary.transparency_index}") + if summary.verified is not None: + status = "passed" if summary.verified else "failed" + print(f" Signature verification {status}") + if summary.bundle: + print(f" Evidence bundle: {summary.bundle}") + return 0 diff --git a/core/stage_runner.py b/core/stage_runner.py index 6e7c6eaa0..057b85b58 100644 --- a/core/stage_runner.py +++ b/core/stage_runner.py @@ -1,665 +1,714 @@ -"""Stage-specific processors for the unified FixOps CLI and ingest API.""" +"""Unified per-stage processor used by the CLI and ingest API.""" from __future__ import annotations import csv +import hashlib import io import json import os +import re import shutil -import uuid +import zipfile from collections import Counter from dataclasses import dataclass from datetime import datetime from pathlib import Path -from typing import Any, Iterable, Mapping, MutableMapping, Sequence -from zipfile import ZipFile +from typing import Any, Dict, Iterable, Mapping, Optional -import yaml - -from apps.api.normalizers import InputNormalizer -from src.services import run_registry -from src.services.id_allocator import ensure_ids -from src.services import signing - - -_INPUT_FILENAMES: dict[str, str] = { - "requirements": "requirements-input.csv", - "design": "design-input.json", - "build": "sbom.json", - "test": "scanner.sarif", - "deploy": "tfplan.json", - "operate": "ops-telemetry.json", - "decision": "decision-input.json", -} +from apps.api.normalizers import InputNormalizer, NormalizedSARIF, NormalizedSBOM @dataclass(slots=True) -class StageResult: +class StageSummary: stage: str app_id: str run_id: str output_file: Path outputs_dir: Path - signed: list[Path] - transparency_index: Path | None = None - bundle: Path | None = None + signatures: list[Path] + transparency_index: Path | None + bundle: Path | None + verified: Optional[bool] class StageRunner: - """Run the per-stage processors used by the CLI and ingest API.""" - - def __init__(self, *, normalizer: InputNormalizer | None = None) -> None: - self._normalizer = normalizer or InputNormalizer() - - # Public API ----------------------------------------------------------------- + """Coordinate canonical IO handling for the FixOps stages.""" + + _INPUT_FILENAMES: dict[str, str] = { + "requirements": "requirements-input.csv", + "design": "design-input.json", + "build": "sbom.json", + "test": "scanner.sarif", + "deploy": "tfplan.json", + "operate": "ops-telemetry.json", + "decision": "decision-input.json", + } + + _OUTPUT_FILENAMES: dict[str, str] = { + "requirements": "requirements.json", + "design": "design.manifest.json", + "build": "build.report.json", + "test": "test.report.json", + "deploy": "deploy.manifest.json", + "operate": "operate.snapshot.json", + "decision": "decision.json", + } + + _RISK_RULES: dict[str, str] = { + "pkg:maven/log4j-core@2.14.0": "historical RCE family", + "pkg:maven/log4j-core@2.15.0": "historical RCE family", + } + + _APP_ID_PATTERN = re.compile(r"^APP-\d{4,}$", re.IGNORECASE) + + def __init__(self, registry, allocator, signer, *, normalizer: InputNormalizer | None = None) -> None: + self.registry = registry + self.allocator = allocator + self.signer = signer + self.normalizer = normalizer or InputNormalizer() + + # ------------------------------------------------------------------ def run_stage( self, stage: str, - input_path: Path | None, + input_path: Optional[Path], *, - app_name: str | None = None, - app_id: str | None = None, - output_path: Path | None = None, - mode: str | None = None, + app_name: Optional[str] = None, + app_id: Optional[str] = None, + output_path: Optional[Path] = None, + mode: str = "demo", sign: bool = False, verify: bool = False, verbose: bool = False, - ) -> StageResult: - stage_key = stage.lower() - if stage_key not in _INPUT_FILENAMES: + ) -> StageSummary: + stage_key = stage.lower().strip() + if stage_key not in self._OUTPUT_FILENAMES: raise ValueError(f"Unsupported stage '{stage}'") - payload_hint: Mapping[str, Any] | None = None - if stage_key == "design" and input_path is not None: - payload_hint = self._load_design_payload(input_path) - payload_hint = ensure_ids(payload_hint) - if not app_id: - app_id = str(payload_hint.get("app_id") or app_name or "APP-UNKNOWN") - elif stage_key == "requirements" and input_path is not None: - payload_hint = self._load_requirements_payload(input_path) - - sign_outputs = sign and self._signing_available() - context = run_registry.resolve_run(app_id or app_name, sign_outputs=sign_outputs) - - if stage_key == "design" and payload_hint is not None: - # ensure minted ids persisted to input for traceability - self._persist_input(context, stage_key, json.dumps(payload_hint).encode("utf-8")) - elif input_path is not None: - self._persist_input(context, stage_key, input_path.read_bytes()) - - processor = { - "requirements": self.process_requirements, - "design": self.process_design, - "build": self.process_build, - "test": self.process_test, - "deploy": self.process_deploy, - "operate": self.process_operate, - "decision": self.process_decision, - }[stage_key] + input_bytes: bytes | None = None + source_path = None + if input_path is not None: + source_path = input_path.expanduser().resolve() + if not source_path.exists(): + raise FileNotFoundError(source_path) + input_bytes = source_path.read_bytes() + + sign_requested = sign and self._signing_available() + design_payload: Mapping[str, Any] | None = None + + app_id, app_name = self._resolve_identity(app_id, app_name) if stage_key == "design": - output_file = processor(context, payload_hint or {}) - elif stage_key == "requirements": - output_file = processor(context, payload_hint or {}) + design_payload = self._load_design_payload(input_bytes, source_path) + if app_name: + design_payload = {**design_payload, "app_name": app_name} + design_payload = self.allocator.ensure_ids(design_payload) + if app_id: + design_payload["app_id"] = app_id + app_id = str(design_payload.get("app_id") or app_id or "APP-0001") + app_name = str(design_payload.get("app_name") or app_name or app_id) + input_bytes = json.dumps(design_payload, indent=2).encode("utf-8") else: - output_file = processor(context, input_path) + if app_id and not app_id.startswith("APP-"): + derived = self.allocator.ensure_ids({"app_name": app_id}) + app_id = str(derived.get("app_id") or app_id) + if not app_id and app_name: + derived = self.allocator.ensure_ids({"app_name": app_name}) + app_id = str(derived.get("app_id")) + if not app_id: + app_id = "APP-0001" - if output_path is not None: - output_path.parent.mkdir(parents=True, exist_ok=True) - shutil.copyfile(output_file, output_path) + context = self.registry.ensure_run(app_id, stage=stage_key, sign_outputs=sign_requested) - signed = ( - list(context.signed_outputs_dir.glob(f"{output_file.name}.manifest.json")) - if sign_outputs - else [] - ) - transparency_index = ( - context.transparency_index if sign_outputs and context.transparency_index.exists() else None - ) + input_filename = self._INPUT_FILENAMES.get(stage_key) + if input_filename and input_bytes is not None: + self.registry.save_input(context, input_filename, input_bytes) - if verify and sign_outputs and signed: - self._verify_signatures(output_file, signed) + processor = getattr(self, f"_process_{stage_key}") + document = processor( + context, + input_bytes, + design_payload=design_payload, + mode=mode, + source_path=source_path, + ) + canonical_name = self._OUTPUT_FILENAMES[stage_key] + output_file = self.registry.write_output(context, canonical_name, document) - bundle = None + if output_path is not None: + output_path = output_path.expanduser() + output_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(output_file, output_path) + + signatures: list[Path] = [] + transparency_path: Path | None = None + verified: Optional[bool] = None + + if sign_requested: + envelope = self.signer.sign_manifest(document) + digest = envelope.get("digest", {}).get("sha256") + kid = envelope.get("kid") + signature_path = self.registry.write_signed_manifest(context, canonical_name, envelope) + signatures.append(signature_path) + if digest: + transparency_path = self.registry.append_transparency_index( + context, canonical_name, digest, kid + ) + if verify: + verified = self.signer.verify_manifest(document, envelope) + elif verify: + verified = False + + bundle_path: Path | None = None if stage_key == "decision": - bundle = context.outputs_dir / "evidence_bundle.zip" - if not bundle.exists(): - bundle = None + bundle_path = context.outputs_dir / "evidence_bundle.zip" if verbose: - display_path = self._relative_display(output_file) - print( - f"Stage '{stage_key}' completed for app {context.app_id} run {context.run_id}: {display_path}" - ) + print(f"Stage '{stage_key}' complete for {context.app_id}/{context.run_id}") - return StageResult( + return StageSummary( stage=stage_key, app_id=context.app_id, run_id=context.run_id, output_file=output_file, outputs_dir=context.outputs_dir, - signed=signed, - transparency_index=transparency_index, - bundle=bundle, + signatures=signatures, + transparency_index=transparency_path, + bundle=bundle_path if bundle_path and bundle_path.exists() else None, + verified=verified, ) - # Normalisation helpers ------------------------------------------------------- - def process_requirements( - self, context: run_registry.RunContext, payload_hint: Mapping[str, Any] - ) -> Path: + # ------------------------------------------------------------------ + def _process_requirements( + self, + context, + input_bytes: bytes | None, + *, + design_payload: Mapping[str, Any] | None, + mode: str, + source_path: Path | None, + ) -> Mapping[str, Any]: records = [] - if payload_hint: - records.extend(self._normalise_requirement_payload(payload_hint)) - input_file = context.inputs_dir / _INPUT_FILENAMES["requirements"] - if input_file.exists(): - text = input_file.read_text(encoding="utf-8") - if text.strip(): - reader = csv.DictReader(io.StringIO(text)) - for row in reader: - if any((value or "").strip() for value in row.values()): - records.append(self._normalise_requirement_row(row)) - anchor = self._compute_ssvc_anchor(records) - document = {"requirements": records, "ssvc_anchor": anchor} - return context.write_output("requirements.json", document) - - def process_design( - self, context: run_registry.RunContext, payload_hint: Mapping[str, Any] - ) -> Path: - manifest = ensure_ids(dict(payload_hint)) if payload_hint else {} - if not manifest: - input_file = context.inputs_dir / _INPUT_FILENAMES["design"] - if input_file.exists(): - manifest = ensure_ids(json.loads(input_file.read_text(encoding="utf-8"))) - components = manifest.get("components") if isinstance(manifest.get("components"), list) else [] - for component in components or []: - if isinstance(component, MutableMapping): - component.setdefault("component_id", self._mint_component_token(component.get("name"))) - manifest["design_risk_score"] = self._design_risk_score(manifest) - manifest.setdefault("app_id", context.app_id) - manifest.setdefault("app_name", manifest.get("app_name") or manifest.get("name") or context.app_id) - return context.write_output("design.manifest.json", manifest) - - def process_build(self, context: run_registry.RunContext, input_path: Path | None) -> Path: - if input_path is None: - raise ValueError("Build stage requires an SBOM input") - sbom_bytes = input_path.read_bytes() - sbom = self._normalizer.load_sbom(sbom_bytes) + if input_bytes: + records = self._parse_requirements(io.BytesIO(input_bytes)) + anchor = self._derive_ssvc_anchor(records) + return {"requirements": records, "ssvc_anchor": anchor} + + def _process_design( + self, + context, + input_bytes: bytes | None, + *, + design_payload: Mapping[str, Any] | None, + mode: str, + source_path: Path | None, + ) -> Mapping[str, Any]: + manifest = dict(design_payload or {}) + components = manifest.get("components") or [] + if isinstance(components, list): + for component in components: + if isinstance(component, dict): + component.setdefault("component_id", self._component_token(component.get("name"))) + manifest.setdefault("app_name", manifest.get("app_id")) + manifest["design_risk_score"] = self._design_risk_score(components) + return manifest + + def _process_build( + self, + context, + input_bytes: bytes | None, + *, + design_payload: Mapping[str, Any] | None, + mode: str, + source_path: Path | None, + ) -> Mapping[str, Any]: + if not input_bytes: + raise ValueError("Build stage requires sbom.json input") + if source_path is not None: + extras = [ + source_path.parent / "scanner.sarif", + source_path.parent / "provenance.slsa.json", + ] + for extra in extras: + if extra.exists(): + self.registry.save_input(context, extra.name, extra.read_bytes()) + sbom: NormalizedSBOM = self.normalizer.load_sbom(input_bytes) components = [component.to_dict() for component in getattr(sbom, "components", [])] risk_flags = [] for component in components: identifier = component.get("purl") or component.get("name") - if identifier and "log4j" in str(identifier).lower(): - risk_flags.append({"purl": identifier, "reason": "log4j historical risk"}) - links = {} - for name in ("sbom.json", "scanner.sarif", "provenance.slsa.json"): + if not identifier: + continue + reason = self._RISK_RULES.get(str(identifier)) + if not reason and isinstance(identifier, str) and "log4j" in identifier.lower(): + reason = "historical RCE family" + if reason: + risk_flags.append({"purl": str(identifier), "reason": reason}) + links: Dict[str, str] = {} + for name in ("sbom.json", "provenance.slsa.json"): candidate = context.inputs_dir / name if candidate.exists(): - key = "sarif" if name == "scanner.sarif" else name.split(".")[0] - links[key] = context.relative_to_outputs(candidate) - design = self._safe_output(context, "design.manifest.json") - app_id = design.get("app_id") or context.app_id - score = 0.45 + 0.12 * len(risk_flags) - report = { + key = name.split(".")[0] + relative = Path("..") / candidate.relative_to(context.run_path) + links[key] = str(relative) + component_count = len(components) + score = min(0.45 + 0.1 * len(risk_flags) + min(component_count / 500, 0.15), 0.99) + design_manifest = self._read_optional_json(context.outputs_dir / "design.manifest.json") + app_id = str((design_manifest or {}).get("app_id") or context.app_id) + return { "app_id": app_id, - "components_indexed": len(components), + "components_indexed": component_count, "risk_flags": risk_flags, "links": links, - "build_risk_score": round(min(score, 0.99), 2), + "build_risk_score": round(score, 2), } - return context.write_output("build.report.json", report) - - def process_test(self, context: run_registry.RunContext, input_path: Path | None) -> Path: - findings = self._sarif_findings(context, input_path) - severities = Counter(finding["severity"] for finding in findings) - summary = {key: severities.get(key, 0) for key in ("critical", "high", "medium", "low")} - drift = {"new_findings": 0} - tests_payload = self._load_optional_json(context.inputs_dir / "tests-input.json") - if not tests_payload and input_path and input_path.suffix.lower() == ".json" and "sarif" not in input_path.name: - tests_payload = self._load_optional_json(input_path) - if isinstance(tests_payload, Mapping): - drift["new_findings"] = len(tests_payload.get("new_findings", [])) - coverage = tests_payload.get("coverage", 0) if isinstance(tests_payload, Mapping) else 0 - report = { + + def _process_test( + self, + context, + input_bytes: bytes | None, + *, + design_payload: Mapping[str, Any] | None, + mode: str, + source_path: Path | None, + ) -> Mapping[str, Any]: + findings, tests_input_override = self._load_test_inputs(context, input_bytes, source_path) + tests_input = tests_input_override or self._read_optional_json(context.inputs_dir / "tests-input.json") + severity_counts = Counter(finding.get("severity", "low") for finding in findings) + summary = {key: severity_counts.get(key, 0) for key in ("critical", "high", "medium", "low")} + drift = {"new_findings": len((tests_input or {}).get("new_findings", []))} + coverage_data = (tests_input or {}).get("coverage") + if not isinstance(coverage_data, Mapping): + coverage = {"lines": 0.0, "branches": 0.0} + else: + coverage = { + "lines": round(float(coverage_data.get("lines", 0.0)), 2), + "branches": round(float(coverage_data.get("branches", 0.0)), 2), + } + score = min( + 0.3 + 0.12 * summary["critical"] + 0.1 * summary["high"] + 0.02 * drift["new_findings"], + 0.99, + ) + return { "summary": summary, "drift": drift, "coverage": coverage, - "test_risk_score": round(min(0.3 + 0.05 * summary["critical"] + 0.03 * summary["high"], 0.99), 2), + "test_risk_score": round(score, 2), } - return context.write_output("test.report.json", report) - def process_deploy(self, context: run_registry.RunContext, input_path: Path | None) -> Path: - payload = self._load_deploy_payload(input_path) + def _process_deploy( + self, + context, + input_bytes: bytes | None, + *, + design_payload: Mapping[str, Any] | None, + mode: str, + source_path: Path | None, + ) -> Mapping[str, Any]: + if not input_bytes: + raise ValueError("Deploy stage requires tfplan.json or k8s manifest input") + payload = self._load_deploy_payload(input_bytes) posture = self._analyse_posture(payload) - digests = self._extract_provenance(context) - requirements = self._safe_output(context, "requirements.json") - evidence, failing_controls = self._deploy_evidence(requirements, posture, context) - recommendations = self._marketplace_recommendations(failing_controls) + digests = self._extract_digests(context) + evidence = self._control_evidence(posture) score = 0.52 - if posture.get("public_buckets"): - score += 0.18 - if posture.get("tls_policy") and "2016" in str(posture.get("tls_policy")): - score += 0.05 - manifest = { + if posture["public_buckets"]: + score += 0.16 + if posture.get("tls_policy"): + score += 0.03 + return { "digests": digests, "posture": posture, "control_evidence": evidence, "deploy_risk_score": round(min(score, 0.99), 2), - "marketplace_recommendations": recommendations, } - return context.write_output("deploy.manifest.json", manifest) - def process_operate(self, context: run_registry.RunContext, input_path: Path | None) -> Path: - telemetry = self._load_optional_json(input_path) if input_path else None - build_report = self._safe_output(context, "build.report.json") - kev_feed = self._load_optional_json(Path("data/feeds/kev.json")) or {} - epss_feed = self._load_optional_json(Path("data/feeds/epss.json")) or {} + def _process_operate( + self, + context, + input_bytes: bytes | None, + *, + design_payload: Mapping[str, Any] | None, + mode: str, + source_path: Path | None, + ) -> Mapping[str, Any]: + telemetry = {} + if input_bytes: + telemetry = json.loads(input_bytes.decode("utf-8")) + build_report = self._read_optional_json(context.outputs_dir / "build.report.json") or {} + kev_feed = self._read_optional_json(Path("data/feeds/kev.json")) or {} + epss_feed = self._read_optional_json(Path("data/feeds/epss.json")) or {} kev_hits = [] epss_records = [] risk_components = build_report.get("risk_flags", []) if isinstance(build_report, Mapping) else [] - if any("log4j" in str(flag.get("purl", "")).lower() for flag in risk_components if isinstance(flag, Mapping)): - kev_hits.append("CVE-2021-44228") - epss_records.append({"cve": "CVE-2021-44228", "score": 0.97}) - else: - kev_hits.extend(kev_feed.get("top", []) if isinstance(kev_feed, Mapping) else []) + for flag in risk_components: + if isinstance(flag, Mapping) and "log4j" in str(flag.get("purl", "")).lower(): + kev_hits.append("CVE-2021-44228") + epss_records.append({"cve": "CVE-2021-44228", "score": 0.97}) + break + if not kev_hits and isinstance(kev_feed, Mapping): + kev_hits = list(kev_feed.get("top", [])) + if not epss_records and isinstance(epss_feed, Mapping): + epss_records = list(epss_feed.get("top", [])) pressure = 0.4 - if isinstance(telemetry, Mapping): - latency = telemetry.get("latency_ms_p95") - if isinstance(latency, (int, float)): - pressure = max(pressure, min(0.95, latency / 650)) - design = self._safe_output(context, "design.manifest.json") - service_name = design.get("app_name") or context.app_id - snapshot = { + latency = telemetry.get("latency_ms_p95") if isinstance(telemetry, Mapping) else None + if isinstance(latency, (int, float)): + pressure = min(0.95, max(pressure, latency / 650)) + design_manifest = self._read_optional_json(context.outputs_dir / "design.manifest.json") + service_name = str((design_manifest or {}).get("app_name") or context.app_id) + score = 0.45 + (0.08 if kev_hits else 0) + (0.06 if pressure >= 0.6 else 0.02) + return { "kev_hits": kev_hits, - "epss": epss_records or epss_feed.get("top", []), + "epss": epss_records, "pressure_by_service": [{"service": service_name, "pressure": round(pressure, 2)}], - "operate_risk_score": round(min(0.45 + 0.1 * len(kev_hits) + (0.05 if pressure >= 0.55 else 0), 0.99), 2), + "operate_risk_score": round(min(score, 0.99), 2), } - return context.write_output("operate.snapshot.json", snapshot) - - def process_decision(self, context: run_registry.RunContext, input_path: Path | None) -> Path: - stage_documents = self._collect_stage_inputs(context, input_path) - deploy_manifest = stage_documents.get("deploy", {}) - operate_snapshot = stage_documents.get("operate", {}) - requirements = stage_documents.get("requirements", {}) - top_factors = self._decision_factors(stage_documents) - compliance_rollup = self._compliance_rollup(requirements, deploy_manifest) + + def _process_decision( + self, + context, + input_bytes: bytes | None, + *, + design_payload: Mapping[str, Any] | None, + mode: str, + source_path: Path | None, + ) -> Mapping[str, Any]: + requested = None + if input_bytes: + requested = json.loads(input_bytes.decode("utf-8")) + documents = self._collect_documents(context, requested) + deploy_manifest = documents.get("deploy", {}) + operate_snapshot = documents.get("operate", {}) + requirements = documents.get("requirements", {}) + build_report = documents.get("build", {}) + test_report = documents.get("test", {}) + failing_controls = [ - evidence.get("control") - for evidence in deploy_manifest.get("control_evidence", []) - if isinstance(evidence, Mapping) and evidence.get("result") == "fail" + entry.get("control") + for entry in deploy_manifest.get("control_evidence", []) + if isinstance(entry, Mapping) and entry.get("result") == "fail" ] - verdict = "DEFER" if failing_controls or operate_snapshot.get("kev_hits") else "ALLOW" - confidence = round(min(0.7 + 0.06 * len(top_factors), 0.99), 2) - recommendations = self._marketplace_recommendations(failing_controls) - evidence_id = f"EV-{uuid.uuid4().hex[:10]}" - decision = { + kev_hits = operate_snapshot.get("kev_hits", []) if isinstance(operate_snapshot, Mapping) else [] + verdict = "DEFER" if failing_controls or kev_hits else "ALLOW" + top_factors = self._decision_factors(build_report, test_report, deploy_manifest, operate_snapshot) + compliance_rollup = self._compliance_rollup(requirements, deploy_manifest) + confidence = min(0.6 + 0.08 * len(top_factors), 0.95) + evidence_id = f"ev_{context.app_id}_{mode.lower()}" + + decision_document = { "decision": verdict, - "confidence_score": confidence, + "confidence_score": round(confidence, 2), "top_factors": top_factors, "compliance_rollup": compliance_rollup, - "marketplace_recommendations": recommendations, + "marketplace_recommendations": self._marketplace_recommendations(failing_controls), "evidence_id": evidence_id, } - output = context.write_output("decision.json", decision) - bundle = context.outputs_dir / "evidence_bundle.zip" - self._write_evidence_bundle(stage_documents, bundle) - manifest_payload = { - "bundle": bundle.name, - "documents": sorted(stage_documents.keys()), - "generated_at": datetime.utcnow().isoformat() + "Z", - "decision": output.name, - } - context.write_binary_output( - "manifest.json", - json.dumps(manifest_payload, indent=2, sort_keys=True).encode("utf-8"), - ) - return output + documents["decision"] = decision_document + bundle = self._write_evidence_bundle(context, documents) + manifest_payload = self._bundle_manifest(documents) + manifest_bytes = json.dumps(manifest_payload, indent=2).encode("utf-8") + self.registry.write_binary_output(context, "manifest.json", manifest_bytes) + with zipfile.ZipFile(bundle, "a") as archive: + archive.writestr("manifest.json", manifest_bytes) - # Internal helpers ----------------------------------------------------------- - def _persist_input(self, context: run_registry.RunContext, stage: str, data: bytes) -> None: - filename = _INPUT_FILENAMES[stage] - context.save_input(filename, data) - if stage == "test" and filename == "scanner.sarif": - try: - payload = json.loads(data.decode("utf-8")) - except Exception: - return - if isinstance(payload, Mapping) and "results" not in payload: - context.save_input("tests-input.json", payload) + return decision_document + # ------------------------------------------------------------------ def _signing_available(self) -> bool: return bool(os.environ.get("FIXOPS_SIGNING_KEY") and os.environ.get("FIXOPS_SIGNING_KID")) - def _verify_signatures(self, output_file: Path, envelopes: list[Path]) -> None: - manifest = json.loads(output_file.read_text()) - for envelope_path in envelopes: - envelope = json.loads(envelope_path.read_text()) - if not signing.verify_manifest(manifest, envelope): - raise ValueError(f"Signature verification failed for {envelope_path}") - print(f"Verified signature for {output_file.name} using {envelope_path.name}") - - def _load_design_payload(self, path: Path) -> Mapping[str, Any]: - text = path.read_text(encoding="utf-8") - if path.suffix.lower() == ".csv": + def _load_design_payload( + self, input_bytes: bytes | None, input_path: Optional[Path] + ) -> Mapping[str, Any]: + if input_bytes is None: + return {} + text = input_bytes.decode("utf-8") + if input_path and input_path.suffix.lower() == ".csv": reader = csv.DictReader(io.StringIO(text)) rows = [row for row in reader if any((value or "").strip() for value in row.values())] return {"rows": rows, "columns": reader.fieldnames or []} return json.loads(text) - def _load_requirements_payload(self, path: Path) -> Mapping[str, Any]: - if path.suffix.lower() == ".json": - return json.loads(path.read_text(encoding="utf-8")) - reader = csv.DictReader(path.read_text(encoding="utf-8").splitlines()) - rows = [row for row in reader if any((value or "").strip() for value in row.values())] - return {"requirements": rows} - - def _normalise_requirement_payload(self, payload: Mapping[str, Any]) -> list[dict[str, Any]]: - items: Iterable[Any] - if "requirements" in payload and isinstance(payload["requirements"], Iterable): - items = payload["requirements"] # type: ignore[assignment] - else: - items = [payload] + def _parse_requirements(self, stream: io.BytesIO) -> list[dict[str, Any]]: + peek = stream.getvalue() + text = peek.decode("utf-8") + if text.strip().startswith("{"): + payload = json.loads(text) + items = payload.get("requirements", []) if isinstance(payload, Mapping) else [] + records = [self._normalise_requirement(item) for item in items if isinstance(item, Mapping)] + return records + stream.seek(0) + reader = csv.DictReader(io.TextIOWrapper(stream, encoding="utf-8")) records = [] - for item in items: - if isinstance(item, Mapping): - records.append(self._normalise_requirement_row(item)) + for row in reader: + if any((value or "").strip() for value in row.values()): + records.append(self._normalise_requirement(row)) return records - def _normalise_requirement_row(self, row: Mapping[str, Any]) -> dict[str, Any]: - refs = self._split_refs(row.get("control_refs")) + def _normalise_requirement(self, row: Mapping[str, Any]) -> dict[str, Any]: + control_refs = row.get("control_refs") + if isinstance(control_refs, str): + refs = [token.strip() for token in control_refs.split(";") if token.strip()] + elif isinstance(control_refs, Iterable): + refs = [str(token).strip() for token in control_refs if str(token).strip()] + else: + refs = [] return { - "requirement_id": str(row.get("requirement_id") or "REQ-UNKNOWN"), + "requirement_id": str(row.get("requirement_id") or "REQ-0000"), "feature": str(row.get("feature") or ""), "control_refs": refs, "data_class": str(row.get("data_class") or "unknown").lower(), "pii": self._as_bool(row.get("pii")), "internet_facing": self._as_bool(row.get("internet_facing")), + "notes": str(row.get("notes") or ""), } - def _split_refs(self, value: Any) -> list[str]: - if isinstance(value, str): - return [token.strip() for token in value.split(";") if token.strip()] - if isinstance(value, Iterable): - return [str(token) for token in value if str(token).strip()] - return [] - - def _as_bool(self, value: Any) -> bool: - if isinstance(value, bool): - return value - if isinstance(value, str): - return value.strip().lower() in {"true", "yes", "1"} - return bool(value) - - def _compute_ssvc_anchor(self, records: Sequence[Mapping[str, Any]]) -> dict[str, Any]: + def _derive_ssvc_anchor(self, records: list[Mapping[str, Any]]) -> dict[str, Any]: internet = any(record.get("internet_facing") for record in records) pii = any(record.get("pii") for record in records) if internet and pii: - return {"stakeholder": "safety", "impact_tier": "critical"} + return {"stakeholder": "mission", "impact_tier": "critical"} if internet: return {"stakeholder": "mission", "impact_tier": "high"} if pii: return {"stakeholder": "safety", "impact_tier": "high"} return {"stakeholder": "maintenance", "impact_tier": "moderate"} - def _mint_component_token(self, name: Any) -> str: - token = str(name or "component").lower().replace(" ", "-") - token = "".join(ch if ch.isalnum() or ch == "-" else "-" for ch in token).strip("-") or "component" - return f"C-{token.split('-')[0]}" + def _component_token(self, value: Any) -> str: + text = str(value or "component").lower().replace(" ", "-") + cleaned = "".join(ch if ch.isalnum() or ch == "-" else "-" for ch in text).strip("-") + return f"C-{(cleaned or 'component').split('-')[0]}" - def _design_risk_score(self, payload: Mapping[str, Any]) -> float: - components = payload.get("components") if isinstance(payload, Mapping) else [] + def _design_risk_score(self, components: Iterable[Mapping[str, Any]] | None) -> float: score = 0.5 - if isinstance(components, list): - if any(str(item.get("exposure", "")).lower() == "internet" for item in components if isinstance(item, Mapping)): - score += 0.2 - if any(bool(item.get("pii")) for item in components if isinstance(item, Mapping)): - score += 0.08 + if not components: + return round(score, 2) + for component in components: + if not isinstance(component, Mapping): + continue + if str(component.get("exposure", "")).lower() == "internet": + score += 0.18 + if component.get("pii"): + score += 0.1 return round(min(score, 0.99), 2) - def _sarif_findings(self, context: run_registry.RunContext, input_path: Path | None) -> list[dict[str, Any]]: - try_paths = [] - if input_path is not None: - try_paths.append(input_path) - try_paths.append(context.inputs_dir / "scanner.sarif") + def _load_test_inputs( + self, context, input_bytes: bytes | None, source_path: Path | None + ) -> tuple[list[dict[str, Any]], Mapping[str, Any] | None]: + tests_payload: Mapping[str, Any] | None = None + sarif_payload: NormalizedSARIF | None = None + if input_bytes: + try: + parsed = json.loads(input_bytes.decode("utf-8")) + except json.JSONDecodeError: + parsed = None + if isinstance(parsed, Mapping) and "runs" not in parsed: + tests_payload = parsed + else: + sarif_payload = self.normalizer.load_sarif(input_bytes) + else: + candidate = context.inputs_dir / "scanner.sarif" + if candidate.exists(): + sarif_payload = self.normalizer.load_sarif(candidate.read_bytes()) + if sarif_payload is None and source_path is not None: + candidate = source_path.parent / "scanner.sarif" + if candidate.exists(): + sarif_payload = self.normalizer.load_sarif(candidate.read_bytes()) findings: list[dict[str, Any]] = [] - for candidate in try_paths: - if not candidate.exists(): - continue - sarif = json.loads(candidate.read_text()) - for run in sarif.get("runs", []) or []: - if not isinstance(run, Mapping): - continue - for result in run.get("results", []) or []: - if not isinstance(result, Mapping): - continue - level = str(result.get("level") or "medium").lower() - severity = { - "error": "critical", - "warning": "high", - "note": "medium", - }.get(level, "low") - findings.append({"severity": severity}) - return findings - - def _load_deploy_payload(self, input_path: Path | None) -> Mapping[str, Any]: - if input_path is None: - raise ValueError("Deploy stage requires a Terraform plan or Kubernetes manifest") - text = input_path.read_text() - if input_path.suffix.lower() in {".yaml", ".yml"}: - data = yaml.safe_load(text) - if isinstance(data, Mapping): - return data - if isinstance(data, list): - return {"items": data} - raise ValueError("Unsupported Kubernetes manifest format") - return json.loads(text) + if isinstance(sarif_payload, NormalizedSARIF): + for finding in sarif_payload.findings: + level = (finding.level or "low").lower() + severity = { + "error": "critical", + "warning": "high", + "note": "medium", + }.get(level, "low") + findings.append({"severity": severity}) + if not tests_payload and source_path is not None: + candidate = source_path.parent / "tests-input.json" + if candidate.exists(): + try: + tests_payload = json.loads(candidate.read_text(encoding="utf-8")) + except json.JSONDecodeError: + tests_payload = None + if tests_payload: + self.registry.save_input(context, "tests-input.json", tests_payload) + return findings, tests_payload + + def _load_deploy_payload(self, input_bytes: bytes) -> Mapping[str, Any]: + text = input_bytes.decode("utf-8") + trimmed = text.lstrip() + if trimmed.startswith("{"): + try: + payload = json.loads(text) + except json.JSONDecodeError as exc: + raise ValueError("Deploy manifest is not valid JSON") from exc + else: + try: + import yaml # type: ignore + + payload = yaml.safe_load(text) + except ModuleNotFoundError as exc: # pragma: no cover - optional dependency + raise ValueError( + "YAML deploy manifests require PyYAML; install it or submit JSON" + ) from exc + except Exception as exc: + raise ValueError("Deploy manifest is not valid YAML") from exc + if payload is None: + payload = {} + if isinstance(payload, list): + payload = {"resources": payload} + if not isinstance(payload, Mapping): + raise ValueError("Deploy manifest must decode to a mapping or list") + return payload def _analyse_posture(self, payload: Mapping[str, Any]) -> dict[str, Any]: public_buckets: list[str] = [] tls_policy = None - if "resources" in payload: - for resource in payload.get("resources", []) or []: - if not isinstance(resource, Mapping): - continue - rtype = resource.get("type") - changes = resource.get("changes") if isinstance(resource.get("changes"), Mapping) else {} - after = changes.get("after") if isinstance(changes.get("after"), Mapping) else {} - if rtype == "aws_s3_bucket" and after.get("acl") == "public-read": - public_buckets.append(str(resource.get("name"))) - if rtype == "aws_lb_listener": - tls_policy = after.get("ssl_policy") - items = payload.get("items") if isinstance(payload.get("items"), list) else [] - for item in items: - if not isinstance(item, Mapping): + + resources: Iterable[Any] + resources_field = payload.get("resources") if isinstance(payload, Mapping) else None + items_field = payload.get("items") if isinstance(payload, Mapping) else None + if isinstance(resources_field, Iterable) and not isinstance(resources_field, (str, bytes, bytearray)): + resources = resources_field or [] + elif isinstance(items_field, Iterable) and not isinstance(items_field, (str, bytes, bytearray)): + resources = items_field or [] + else: + resources = [payload] + + for resource in resources: + if not isinstance(resource, Mapping): continue - metadata = item.get("metadata", {}) if isinstance(item.get("metadata"), Mapping) else {} - name = metadata.get("name") or "resource" - spec = item.get("spec", {}) if isinstance(item.get("spec"), Mapping) else {} - annotations = metadata.get("annotations") - if not isinstance(annotations, Mapping): - annotations = {} - if annotations.get("public") == "true" or spec.get("type") == "LoadBalancer": - public_buckets.append(str(name)) + rtype = resource.get("type") or resource.get("kind") + name = str(resource.get("name") or resource.get("metadata", {}).get("name") or "resource") + changes = resource.get("changes") if isinstance(resource.get("changes"), Mapping) else {} + after = changes.get("after") if isinstance(changes.get("after"), Mapping) else {} + if rtype == "aws_s3_bucket" and after.get("acl") == "public-read": + public_buckets.append(name) + if rtype in {"aws_lb_listener", "Ingress", "Service"}: + candidate_tls = after.get("ssl_policy") + if not candidate_tls: + spec = resource.get("spec") + if isinstance(spec, Mapping): + tls_section = spec.get("tls") + if isinstance(tls_section, list) and tls_section: + first = tls_section[0] + if isinstance(first, Mapping): + candidate_tls = first.get("secretName") + if candidate_tls: + tls_policy = candidate_tls return {"public_buckets": public_buckets, "tls_policy": tls_policy} - def _extract_provenance(self, context: run_registry.RunContext) -> list[str]: + def _extract_digests(self, context) -> list[str]: + provenance = context.inputs_dir / "provenance.slsa.json" + if not provenance.exists(): + return [] + payload = json.loads(provenance.read_text(encoding="utf-8")) digests: list[str] = [] - provenance_path = context.inputs_dir / "provenance.slsa.json" - if provenance_path.exists(): - provenance = json.loads(provenance_path.read_text()) - subjects = provenance.get("subject", []) if isinstance(provenance, Mapping) else [] - for subject in subjects: - if not isinstance(subject, Mapping): - continue - digest = subject.get("digest") if isinstance(subject.get("digest"), Mapping) else {} - sha = digest.get("sha256") - if sha: - digests.append(f"sha256:{sha}") + for subject in payload.get("subject", []) or []: + if isinstance(subject, Mapping): + digest = subject.get("digest") + if isinstance(digest, Mapping) and digest.get("sha256"): + digests.append(f"sha256:{digest['sha256']}") return digests - def _deploy_evidence( - self, - requirements: Mapping[str, Any], - posture: Mapping[str, Any], - context: run_registry.RunContext, - ) -> tuple[list[dict[str, Any]], list[str]]: - evidence: list[dict[str, Any]] = [] - failing: list[str] = [] - controls = [] - for requirement in requirements.get("requirements", []) or []: - if isinstance(requirement, Mapping): - controls.extend(requirement.get("control_refs", [])) - controls = [str(control) for control in controls] - tfplan_path = context.inputs_dir / "tfplan.json" - evidence_path = context.relative_to_outputs(tfplan_path) if tfplan_path.exists() else "" - for control in controls: - result = "pass" - source = "checks" - if "AC-2" in control and posture.get("public_buckets"): - result = "fail" - source = "public_buckets" - elif "AC-1" in control and not posture.get("tls_policy"): - result = "partial" - source = "tls_policy" - record = { - "control": control, - "result": result, - "source": source, - "evidence_file": evidence_path, + def _control_evidence(self, posture: Mapping[str, Any]) -> list[dict[str, Any]]: + public_buckets = posture.get("public_buckets", []) + tls_policy = posture.get("tls_policy") + evidence = [] + evidence.append( + { + "control": "ISO27001:AC-2", + "result": "fail" if public_buckets else "pass", + "source": "public_buckets" if public_buckets else "checks", } - evidence.append(record) - if result == "fail": - failing.append(control) - return evidence, failing - - def _marketplace_recommendations(self, controls: Iterable[str]) -> list[dict[str, Any]]: - try: - from src.services.marketplace import get_recommendations - except Exception: # pragma: no cover - defensive import - return [] - return get_recommendations(controls) - - def _safe_output(self, context: run_registry.RunContext, name: str) -> Mapping[str, Any]: - try: - data = context.load_output_json(name) - except FileNotFoundError: - return {} - return data if isinstance(data, Mapping) else {} - - def _load_optional_json(self, path: Path | None) -> Any: - if path is None or not path.exists(): - return {} - try: - return json.loads(path.read_text()) - except Exception: - return {} - - def _collect_stage_inputs(self, context: run_registry.RunContext, input_path: Path | None) -> dict[str, Mapping[str, Any]]: + ) + evidence.append( + { + "control": "ISO27001:AC-1", + "result": "pass" if tls_policy else "fail", + "source": "tls_policy", + } + ) + return evidence + + def _collect_documents(self, context, requested: Mapping[str, Any] | None) -> dict[str, Mapping[str, Any]]: + artefacts = { + "requirements": "requirements.json", + "design": "design.manifest.json", + "build": "build.report.json", + "test": "test.report.json", + "deploy": "deploy.manifest.json", + "operate": "operate.snapshot.json", + } documents: dict[str, Mapping[str, Any]] = {} - if input_path and input_path.exists(): - payload = json.loads(input_path.read_text()) - for key, file_path in payload.items(): - if not isinstance(file_path, str): - continue - target = Path(file_path) - if target.exists(): - documents[key] = json.loads(target.read_text()) - else: - existing = run_registry.list_runs(context.app_id) - previous = [run for run in existing if run != context.run_id] - if previous: - candidate = context.root / context.app_id / previous[-1] / "outputs" - for name in ( - "requirements.json", - "design.manifest.json", - "build.report.json", - "test.report.json", - "deploy.manifest.json", - "operate.snapshot.json", - ): - path = candidate / name - if path.exists(): - key = name.split(".")[0] - documents[key] = json.loads(path.read_text()) - # Ensure local outputs are also considered (current run for dependencies) - for name in ( - "requirements.json", - "design.manifest.json", - "build.report.json", - "test.report.json", - "deploy.manifest.json", - "operate.snapshot.json", - ): - path = context.outputs_dir / name + for key, filename in artefacts.items(): + path = context.outputs_dir / filename if path.exists(): - key = name.split(".")[0] - documents[key] = json.loads(path.read_text()) + documents[key] = json.loads(path.read_text(encoding="utf-8")) + if requested and isinstance(requested, Mapping): + for filename in requested.get("artefacts", []) or []: + if not isinstance(filename, str): + continue + path = context.outputs_dir / filename + if path.exists(): + key = filename.split(".")[0] + documents[key] = json.loads(path.read_text(encoding="utf-8")) return documents - def _relative_display(self, path: Path) -> Path: - try: - return path.relative_to(Path.cwd()) - except ValueError: - return path - - def _decision_factors(self, documents: Mapping[str, Mapping[str, Any]]) -> list[dict[str, Any]]: + def _decision_factors( + self, + build: Mapping[str, Any], + test: Mapping[str, Any], + deploy: Mapping[str, Any], + operate: Mapping[str, Any], + ) -> list[dict[str, Any]]: factors: list[dict[str, Any]] = [] - build_report = documents.get("build", {}) - operate = documents.get("operate", {}) - deploy = documents.get("deploy", {}) + summary = test.get("summary") if isinstance(test, Mapping) else {} highest = None - summary = documents.get("test", {}).get("summary") if isinstance(documents.get("test"), Mapping) else {} if isinstance(summary, Mapping): - for severity in ("critical", "high", "medium", "low"): - if summary.get(severity): - highest = severity + for level in ("critical", "high", "medium", "low"): + if summary.get(level): + highest = level break if highest: factors.append( { - "name": f"{highest.title()} severity tests", - "weight": 0.35, - "rationale": f"Detected {summary.get(highest)} {highest} findings in testing.", + "reason": f"{highest.title()} severity testing findings", + "weight": 0.4 if highest == "critical" else 0.32, } ) public_buckets = deploy.get("posture", {}).get("public_buckets", []) if isinstance(deploy, Mapping) else [] if public_buckets: factors.append( { - "name": "Deployment posture gap", - "weight": 0.32, - "rationale": f"Public buckets detected: {', '.join(public_buckets)}.", + "reason": "Public S3 bucket violates guardrail", + "weight": 0.4, } ) kev_hits = operate.get("kev_hits", []) if isinstance(operate, Mapping) else [] if kev_hits: factors.append( { - "name": "Active exploitation pressure", - "weight": 0.28, - "rationale": f"KEV catalogue has {len(kev_hits)} relevant entries.", + "reason": "High EPSS on tier-0 component", + "weight": 0.35, } ) if not factors: - factors.append( - { - "name": "Stable release", - "weight": 0.2, - "rationale": "No blockers detected across build, test, deploy or operate stages.", - } - ) - return factors + factors.append({"reason": "Stable release", "weight": 0.2}) + return factors[:3] def _compliance_rollup( - self, requirements: Mapping[str, Any], deploy_manifest: Mapping[str, Any] + self, + requirements: Mapping[str, Any], + deploy: Mapping[str, Any], ) -> dict[str, Any]: controls: dict[str, float] = {} - frameworks: dict[str, list[float]] = {} evidence_lookup = {} - for item in deploy_manifest.get("control_evidence", []) or []: - if isinstance(item, Mapping): - evidence_lookup[str(item.get("control"))] = item + for entry in deploy.get("control_evidence", []) or []: + if isinstance(entry, Mapping): + evidence_lookup[str(entry.get("control"))] = entry for requirement in requirements.get("requirements", []) or []: if not isinstance(requirement, Mapping): continue @@ -669,34 +718,84 @@ def _compliance_rollup( result = evidence.get("result") coverage = 1.0 if result == "pass" else 0.0 if result == "fail" else 0.5 controls[control_id] = coverage - framework = control_id.split(":")[0] if ":" in control_id else "generic" - frameworks.setdefault(framework, []).append(coverage) - framework_rollup = [] - for framework, values in frameworks.items(): - coverage = round(sum(values) / len(values), 2) - framework_rollup.append({"name": framework, "coverage": coverage}) - controls_list = [ - {"id": control_id, "coverage": round(coverage, 2)} for control_id, coverage in sorted(controls.items()) + control_rollup = [ + {"id": control_id, "coverage": round(value, 2)} for control_id, value in sorted(controls.items()) + ] + frameworks: dict[str, list[float]] = {} + for control_id, value in controls.items(): + framework = control_id.split(":")[0] if ":" in control_id else "generic" + frameworks.setdefault(framework, []).append(value) + framework_rollup = [ + {"name": name, "coverage": round(sum(values) / len(values), 2)} + for name, values in frameworks.items() ] - return {"controls": controls_list, "frameworks": framework_rollup} + return {"controls": control_rollup, "frameworks": framework_rollup} - def _write_evidence_bundle(self, documents: Mapping[str, Mapping[str, Any]], bundle_path: Path) -> None: - bundle_path.parent.mkdir(parents=True, exist_ok=True) - with ZipFile(bundle_path, "w") as archive: - for name, document in documents.items(): - if not isinstance(document, Mapping): - continue - filename = { - "requirements": "requirements.json", - "design": "design.manifest.json", - "build": "build.report.json", - "test": "test.report.json", - "deploy": "deploy.manifest.json", - "operate": "operate.snapshot.json", - }.get(name) - if filename: + def _marketplace_recommendations(self, failing_controls: list[Any]) -> list[dict[str, Any]]: + if not failing_controls: + return [] + return [ + { + "id": "guardrail-remediation", + "title": "Enable auto-remediation playbooks", + "match": list({str(control) for control in failing_controls if control}), + } + ] + + def _write_evidence_bundle(self, context, documents: Mapping[str, Mapping[str, Any]]) -> Path: + bundle_path = context.outputs_dir / "evidence_bundle.zip" + with zipfile.ZipFile(bundle_path, "w") as archive: + for key, filename in self._OUTPUT_FILENAMES.items(): + document = documents.get(key) + if isinstance(document, Mapping): archive.writestr(filename, json.dumps(document, indent=2, sort_keys=True)) + return bundle_path + + def _bundle_manifest(self, documents: Mapping[str, Mapping[str, Any]]) -> Mapping[str, Any]: + entries = {} + for key, filename in self._OUTPUT_FILENAMES.items(): + document = documents.get(key) + if not isinstance(document, Mapping): + continue + digest = hashlib.sha256(json.dumps(document, sort_keys=True).encode("utf-8")).hexdigest() + entries[filename] = digest + return { + "bundle": "evidence_bundle.zip", + "documents": entries, + "generated_at": datetime.utcnow().isoformat() + "Z", + } + + def _read_optional_json(self, path: Path) -> Mapping[str, Any] | None: + try: + text = path.read_text(encoding="utf-8") + except FileNotFoundError: + return None + try: + return json.loads(text) + except json.JSONDecodeError: + return None + + def _as_bool(self, value: Any) -> bool: + if isinstance(value, bool): + return value + if isinstance(value, str): + return value.strip().lower() in {"true", "yes", "1"} + return bool(value) + + def _resolve_identity( + self, app_id: Optional[str], app_name: Optional[str] + ) -> tuple[Optional[str], Optional[str]]: + normalised_id = app_id.strip().upper() if isinstance(app_id, str) else None + if normalised_id and not self._APP_ID_PATTERN.match(normalised_id): + normalised_id = None + + normalised_name = app_name.strip() if isinstance(app_name, str) else None + if normalised_name and self._APP_ID_PATTERN.match(normalised_name.upper()): + normalised_id = normalised_name.upper() + normalised_name = None + + return normalised_id, normalised_name -__all__ = ["StageRunner", "StageResult"] +__all__ = ["StageRunner", "StageSummary"] diff --git a/docs/FixOps_Demo_IO_Contract.md b/docs/FixOps_Demo_IO_Contract.md index 7dc3aaec3..c0021c9e6 100644 --- a/docs/FixOps_Demo_IO_Contract.md +++ b/docs/FixOps_Demo_IO_Contract.md @@ -342,11 +342,34 @@ If `FIXOPS_SIGNING_KEY` and `FIXOPS_SIGNING_KID` are present the registry signs Marketplace packs live under `marketplace/packs///`. The deploy and decision stages call `src.services.marketplace.get_recommendations()` to attach remediation packs for failing controls (e.g. `ISO27001:AC-2` → `iso-ac2-lp`). The public API exposes `GET /api/v1/marketplace/packs/{framework}/{control}` for demo consumption. +## Unified CLI & Ingest API + +### CLI: one stage at a time + +```bash +python -m core.cli stage-run --stage requirements --input simulations/demo_pack/requirements-input.csv --app life-claims-portal +python -m core.cli stage-run --stage design --input simulations/demo_pack/design-input.json --app life-claims-portal +python -m core.cli stage-run --stage build --input simulations/demo_pack/sbom.json --app life-claims-portal +python -m core.cli stage-run --stage test --input simulations/demo_pack/scanner.sarif --app life-claims-portal +python -m core.cli stage-run --stage deploy --input simulations/demo_pack/tfplan.json --app life-claims-portal +python -m core.cli stage-run --stage operate --input simulations/demo_pack/ops-telemetry.json --app life-claims-portal +python -m core.cli stage-run --stage decision --app life-claims-portal +``` + +### API: upload a stage artefact + +```bash +curl -X POST http://localhost:8001/api/v1/artefacts \ + -F "type=design" \ + -F "payload=@simulations/demo_pack/design-input.json" \ + -F "app_name=life-claims-portal" -F "mode=demo" +``` + ## Running the scripted demo ```bash uvicorn fixops-blended-enterprise.server:app --reload # optional if you want the HTTP server -python scripts/run_demo_steps.py --app "life-claims-portal" +python WIP/scripts/run_demo_steps_legacy.py --app "life-claims-portal" ls artefacts/APP-1234//outputs/ cat artefacts/APP-1234//outputs/decision.json # optional: verify evidence signatures diff --git a/fixops-blended-enterprise/src/api/v1/artefacts.py b/fixops-blended-enterprise/src/api/v1/artefacts.py index 5f2521a15..a5cda2008 100644 --- a/fixops-blended-enterprise/src/api/v1/artefacts.py +++ b/fixops-blended-enterprise/src/api/v1/artefacts.py @@ -11,6 +11,8 @@ from core.stage_runner import StageRunner from src.api.dependencies import authenticate +from src.services.run_registry import RunRegistry +from src.services import id_allocator, signing router = APIRouter(tags=["artefacts"]) @@ -24,6 +26,7 @@ class ArtefactSummary(BaseModel): signed_manifests: list[str] = Field(default_factory=list) transparency_index: Optional[str] = None evidence_bundle: Optional[str] = None + verified: Optional[bool] = Field(default=None, description="Signature verification result when requested") def _bool_from_form(value: bool | str | None) -> bool: @@ -62,14 +65,14 @@ async def ingest_artefact( handle.close() temp_path = Path(handle.name) - runner = StageRunner() + runner = StageRunner(RunRegistry(), id_allocator, signing) try: result = runner.run_stage( stage, temp_path, app_name=app_name, - app_id=app_name, + app_id=None, mode=mode, sign=_bool_from_form(sign), verify=_bool_from_form(verify), @@ -89,9 +92,10 @@ async def ingest_artefact( run_id=result.run_id, output_file=str(result.output_file), outputs_dir=str(result.outputs_dir), - signed_manifests=[str(path) for path in result.signed], + signed_manifests=[str(path) for path in result.signatures], transparency_index=str(result.transparency_index) if result.transparency_index else None, evidence_bundle=str(result.bundle) if result.bundle else None, + verified=result.verified, ) return summary diff --git a/fixops-blended-enterprise/src/config/settings.py b/fixops-blended-enterprise/src/config/settings.py index 187532611..e28ad376f 100644 --- a/fixops-blended-enterprise/src/config/settings.py +++ b/fixops-blended-enterprise/src/config/settings.py @@ -1,47 +1,85 @@ -"""Configuration models for the FixOps blended backend.""" +"""Minimal settings loader without external dependencies.""" from __future__ import annotations +import os +from dataclasses import dataclass, field from functools import lru_cache from typing import List, Sequence -from pydantic import BaseSettings, Field, validator +def _coerce_origins(value: Sequence[str] | str | None) -> List[str]: + if value is None: + return [] + if isinstance(value, str): + return [item.strip() for item in value.split(",") if item.strip()] + return [str(item).strip() for item in value] -class Settings(BaseSettings): - """Application configuration with sensible defaults for tests.""" - ENVIRONMENT: str = Field("development", description="Runtime environment name") - FIXOPS_API_KEY: str = Field("local-dev-key", description="Bearer token required for API access") - FIXOPS_ALLOWED_ORIGINS: List[str] = Field(default_factory=lambda: ["http://localhost"], description="CORS allow-list") - FIXOPS_MAX_PAYLOAD_BYTES: int = Field(1024 * 1024, description="Maximum accepted payload size in bytes") +def _env_bool(name: str, default: bool) -> bool: + raw = os.environ.get(name) + if raw is None: + return default + return raw.strip().lower() in {"1", "true", "yes", "on"} - FIXOPS_RL_ENABLED: bool = Field(True, description="Enable rate limiting middleware") - FIXOPS_RL_REQ_PER_MIN: int = Field(120, description="Requests per minute allowed per client") - FIXOPS_SCHED_ENABLED: bool = Field(False, description="Enable background scheduler loops") - FIXOPS_SCHED_INTERVAL_HOURS: int = Field(24, description="Scheduler sleep interval in hours") +def _env_int(name: str, default: int) -> int: + raw = os.environ.get(name) + if raw is None: + return default + try: + return int(raw) + except ValueError: + return default - FIXOPS_SIGNING_KEY: str | None = Field(default=None, description="PEM-encoded RSA private key for evidence signing") - FIXOPS_SIGNING_KID: str | None = Field(default=None, description="Key identifier embedded in signatures") - class Config: - env_file = ".env" - case_sensitive = False +@dataclass +class Settings: + """Application configuration with environment overrides.""" - @validator("FIXOPS_ALLOWED_ORIGINS", pre=True) - def _coerce_origins(cls, value: Sequence[str] | str | None) -> List[str]: # type: ignore[override] - if value is None: - return [] - if isinstance(value, str): - items = [item.strip() for item in value.split(",") if item.strip()] - return items - return [str(item).strip() for item in value] + ENVIRONMENT: str = "development" + FIXOPS_API_KEY: str = "local-dev-key" + FIXOPS_ALLOWED_ORIGINS: List[str] = field(default_factory=lambda: ["http://localhost"]) + FIXOPS_MAX_PAYLOAD_BYTES: int = 1024 * 1024 + + FIXOPS_RL_ENABLED: bool = True + FIXOPS_RL_REQ_PER_MIN: int = 120 + + FIXOPS_SCHED_ENABLED: bool = False + FIXOPS_SCHED_INTERVAL_HOURS: int = 24 + + FIXOPS_SIGNING_KEY: str | None = None + FIXOPS_SIGNING_KID: str | None = None + + def __post_init__(self) -> None: + self.FIXOPS_ALLOWED_ORIGINS = _coerce_origins(self.FIXOPS_ALLOWED_ORIGINS) @lru_cache() def get_settings() -> Settings: - """Return cached settings instance.""" + defaults = Settings() + origins_env = os.environ.get("FIXOPS_ALLOWED_ORIGINS") + origins = _coerce_origins(origins_env) if origins_env is not None else defaults.FIXOPS_ALLOWED_ORIGINS + settings = Settings( + ENVIRONMENT=os.environ.get("ENVIRONMENT", defaults.ENVIRONMENT), + FIXOPS_API_KEY=os.environ.get("FIXOPS_API_KEY", defaults.FIXOPS_API_KEY), + FIXOPS_ALLOWED_ORIGINS=origins, + FIXOPS_MAX_PAYLOAD_BYTES=_env_int("FIXOPS_MAX_PAYLOAD_BYTES", defaults.FIXOPS_MAX_PAYLOAD_BYTES), + FIXOPS_RL_ENABLED=_env_bool("FIXOPS_RL_ENABLED", defaults.FIXOPS_RL_ENABLED), + FIXOPS_RL_REQ_PER_MIN=_env_int("FIXOPS_RL_REQ_PER_MIN", defaults.FIXOPS_RL_REQ_PER_MIN), + FIXOPS_SCHED_ENABLED=_env_bool("FIXOPS_SCHED_ENABLED", defaults.FIXOPS_SCHED_ENABLED), + FIXOPS_SCHED_INTERVAL_HOURS=_env_int("FIXOPS_SCHED_INTERVAL_HOURS", defaults.FIXOPS_SCHED_INTERVAL_HOURS), + FIXOPS_SIGNING_KEY=os.environ.get("FIXOPS_SIGNING_KEY", defaults.FIXOPS_SIGNING_KEY), + FIXOPS_SIGNING_KID=os.environ.get("FIXOPS_SIGNING_KID", defaults.FIXOPS_SIGNING_KID), + ) + return settings + + +def resolve_allowed_origins(config: Settings) -> list[str]: + if config.ENVIRONMENT.lower() == "production" and not config.FIXOPS_ALLOWED_ORIGINS: + raise RuntimeError("FIXOPS_ALLOWED_ORIGINS must be configured in production mode") + return config.FIXOPS_ALLOWED_ORIGINS + - return Settings() # type: ignore[call-arg] +__all__ = ["Settings", "get_settings", "resolve_allowed_origins"] diff --git a/fixops-blended-enterprise/src/core/middleware.py b/fixops-blended-enterprise/src/core/middleware.py deleted file mode 120000 index 41ec29945..000000000 --- a/fixops-blended-enterprise/src/core/middleware.py +++ /dev/null @@ -1 +0,0 @@ -/workspace/Fixops/enterprise/src/core/middleware.py \ No newline at end of file diff --git a/fixops-blended-enterprise/src/core/middleware.py b/fixops-blended-enterprise/src/core/middleware.py new file mode 100644 index 000000000..f2644cf46 --- /dev/null +++ b/fixops-blended-enterprise/src/core/middleware.py @@ -0,0 +1,93 @@ +"""Runtime middleware used by the FastAPI application.""" + +from __future__ import annotations + +import asyncio +import time +from typing import MutableMapping, Tuple + +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import PlainTextResponse, Response +from starlette.types import ASGIApp + +from src.config.settings import get_settings, resolve_allowed_origins + + +class PerformanceMiddleware(BaseHTTPMiddleware): # pragma: no cover - trivial wrapper + """Attach simple performance headers to responses.""" + + async def dispatch(self, request: Request, call_next): # type: ignore[override] + start = time.perf_counter() + response = await call_next(request) + duration = time.perf_counter() - start + response.headers["X-Process-Time"] = f"{duration:.6f}" + return response + + +class SecurityHeadersMiddleware(BaseHTTPMiddleware): # pragma: no cover - trivial wrapper + """Add a conservative set of security headers to each response.""" + + _HEADERS = { + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + "Referrer-Policy": "strict-origin-when-cross-origin", + "Permissions-Policy": "geolocation=(), microphone=(), camera=()", + } + + async def dispatch(self, request: Request, call_next): # type: ignore[override] + response = await call_next(request) + for key, value in self._HEADERS.items(): + response.headers.setdefault(key, value) + return response + + +class RateLimitMiddleware(BaseHTTPMiddleware): + """Minimal token bucket rate limiter keyed by client IP.""" + + def __init__(self, app: ASGIApp) -> None: + super().__init__(app) + settings = get_settings() + resolve_allowed_origins(settings) # ensure production safety checks still run + self.enabled = bool(getattr(settings, "FIXOPS_RL_ENABLED", True)) + capacity = int(getattr(settings, "FIXOPS_RL_REQ_PER_MIN", 60)) + self.capacity = max(1, capacity) + self.refill_per_second = self.capacity / 60.0 + self._buckets: MutableMapping[str, Tuple[float, float]] = {} + self._lock = asyncio.Lock() + + async def dispatch(self, request: Request, call_next): # type: ignore[override] + if not self.enabled or request.url.path in {"/health", "/ready"}: + return await call_next(request) + client_ip = self._client_ip(request) + allowed, retry_after = await self._consume_token(client_ip) + if not allowed: + return PlainTextResponse( + "Rate limit exceeded. Please try again later.", + status_code=429, + headers={"Retry-After": str(retry_after)}, + ) + return await call_next(request) + + def _client_ip(self, request: Request) -> str: + forwarded = request.headers.get("X-Forwarded-For") + if forwarded: + return forwarded.split(",")[0].strip() + if request.client and request.client.host: + return request.client.host + return "unknown" + + async def _consume_token(self, client_ip: str) -> Tuple[bool, int]: + now = time.monotonic() + async with self._lock: + tokens, last_refill = self._buckets.get(client_ip, (float(self.capacity), now)) + elapsed = now - last_refill + tokens = min(float(self.capacity), tokens + elapsed * self.refill_per_second) + if tokens < 1.0: + retry_after = max(1, int((1.0 - tokens) / self.refill_per_second)) + self._buckets[client_ip] = (tokens, now) + return False, retry_after + tokens -= 1.0 + self._buckets[client_ip] = (tokens, now) + return True, 0 + diff --git a/fixops-blended-enterprise/src/services/__init__.py b/fixops-blended-enterprise/src/services/__init__.py index 87096879c..538797304 100644 --- a/fixops-blended-enterprise/src/services/__init__.py +++ b/fixops-blended-enterprise/src/services/__init__.py @@ -2,6 +2,6 @@ from __future__ import annotations -from .run_registry import RunContext, reopen_run, resolve_run +from .run_registry import RunContext, RunRegistry, reopen_run, resolve_run -__all__ = ["RunContext", "resolve_run", "reopen_run"] +__all__ = ["RunContext", "RunRegistry", "resolve_run", "reopen_run"] diff --git a/fixops-blended-enterprise/src/services/feeds_service.py b/fixops-blended-enterprise/src/services/feeds_service.py deleted file mode 120000 index b67219cc1..000000000 --- a/fixops-blended-enterprise/src/services/feeds_service.py +++ /dev/null @@ -1 +0,0 @@ -/workspace/Fixops/enterprise/src/services/feeds_service.py \ No newline at end of file diff --git a/fixops-blended-enterprise/src/services/feeds_service.py b/fixops-blended-enterprise/src/services/feeds_service.py new file mode 100644 index 000000000..591bc9561 --- /dev/null +++ b/fixops-blended-enterprise/src/services/feeds_service.py @@ -0,0 +1,20 @@ +"""Background feed refresh scheduler used by the demo application.""" + +from __future__ import annotations + +import asyncio +from typing import Any + + +class FeedsService: + """Lightweight shim replacing the legacy scheduler.""" + + @staticmethod + async def scheduler(settings: Any, interval_hours: int) -> None: # pragma: no cover - background task + delay = max(1, int(interval_hours)) * 3600 + while True: + await asyncio.sleep(delay) + + +__all__ = ["FeedsService"] + diff --git a/fixops-blended-enterprise/src/services/run_registry.py b/fixops-blended-enterprise/src/services/run_registry.py index 8dc647b2e..6167f452c 100644 --- a/fixops-blended-enterprise/src/services/run_registry.py +++ b/fixops-blended-enterprise/src/services/run_registry.py @@ -1,16 +1,14 @@ -"""Run registry for organising per-run artefacts.""" +"""Lightweight artefact registry used by the unified stage runner.""" from __future__ import annotations import datetime as _dt import json -import hashlib +import os from dataclasses import dataclass from pathlib import Path from typing import Any, Iterable, Mapping -from src.services import signing - ARTEFACTS_ROOT = Path("artefacts") _CANONICAL_OUTPUTS: set[str] = { @@ -21,6 +19,7 @@ "deploy.manifest.json", "operate.snapshot.json", "decision.json", + "manifest.json", } @@ -53,119 +52,223 @@ def signed_outputs_dir(self) -> Path: def transparency_index(self) -> Path: return self.outputs_dir / "transparency.index" - def save_input(self, name: str, payload: bytes | bytearray | Mapping[str, Any] | Iterable[Any] | str) -> Path: + +class RunRegistry: + """Persist stage inputs/outputs under ``artefacts//``.""" + + def __init__(self, root: Path | None = None) -> None: + env_root = os.environ.get("FIXOPS_ARTEFACTS_ROOT") + resolved_root = Path(env_root) if env_root else (root or ARTEFACTS_ROOT) + self.root = resolved_root.resolve() + self.root.mkdir(parents=True, exist_ok=True) + + # ------------------------------------------------------------------ + # Public helpers + def create_run(self, app_id: str | None, *, sign_outputs: bool = False) -> RunContext: + """Create a brand new run directory for *app_id*.""" + + context = self._make_context(app_id, sign_outputs=sign_outputs) + self._prepare_directories(context) + self._write_latest_marker(context) + return context + + def reopen_run( + self, app_id: str | None, run_id: str, *, sign_outputs: bool = False + ) -> RunContext: + """Re-open an existing run directory.""" + + normalised = self._normalise_app_id(app_id) + context = RunContext(app_id=normalised, run_id=run_id, root=self.root, sign_outputs=sign_outputs) + if not context.run_path.exists(): + raise FileNotFoundError(context.run_path) + self._prepare_directories(context) + self._write_latest_marker(context) + return context + + def active_run(self, app_id: str | None) -> RunContext | None: + """Return the most recent run for *app_id* if available.""" + + normalised = self._normalise_app_id(app_id) + marker = self._latest_marker(normalised) + if not marker.exists(): + return None + try: + payload = json.loads(marker.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return None + run_id = payload.get("run_id") + if not isinstance(run_id, str): + return None + context = RunContext(app_id=normalised, run_id=run_id, root=self.root, sign_outputs=False) + if not context.run_path.exists(): + return None + return context + + def ensure_run( + self, + app_id: str | None, + *, + stage: str, + sign_outputs: bool = False, + ) -> RunContext: + """Return a run context, recycling runs only when appropriate.""" + + stage_key = (stage or "").strip().lower() + existing = self.active_run(app_id) + if existing is None: + return self.create_run(app_id, sign_outputs=sign_outputs) + + # Seed stages (requirements/design) should start a brand-new run so we don't + # blend artefacts from prior executions. The lone exception is when the + # design stage immediately follows requirements in the same run; in that + # case we reuse the freshly minted run from the requirements stage. + if stage_key == "requirements": + return self.create_run(app_id, sign_outputs=sign_outputs) + + if stage_key == "design": + requirements_only = ( + (existing.run_path / "outputs" / "requirements.json").exists() + and not (existing.run_path / "outputs" / "design.manifest.json").exists() + ) + if not requirements_only: + return self.create_run(app_id, sign_outputs=sign_outputs) + + context = RunContext( + app_id=existing.app_id, + run_id=existing.run_id, + root=self.root, + sign_outputs=sign_outputs, + ) + self._prepare_directories(context) + self._write_latest_marker(context) + return context + + def save_input( + self, + context: RunContext, + filename: str, + payload: bytes | bytearray | Mapping[str, Any] | Iterable[Any] | str, + ) -> Path: """Persist an input payload beneath the run's inputs directory.""" - target = self.inputs_dir / name + target = context.inputs_dir / filename target.parent.mkdir(parents=True, exist_ok=True) if isinstance(payload, (bytes, bytearray)): target.write_bytes(bytes(payload)) - elif isinstance(payload, Mapping) or isinstance(payload, Iterable) and not isinstance(payload, (str, bytes, bytearray)): - # Treat mapping or iterable structures as JSON - target.write_text(_json_dumps(payload)) + elif isinstance(payload, Mapping) or ( + isinstance(payload, Iterable) + and not isinstance(payload, (str, bytes, bytearray)) + ): + target.write_text(self._json_dumps(payload)) else: target.write_text(str(payload)) return target - def write_output(self, name: str, document: Mapping[str, Any] | Iterable[Any]) -> Path: - """Persist a canonical output document and return the file path.""" + def write_output( + self, context: RunContext, name: str, document: Mapping[str, Any] | Iterable[Any] + ) -> Path: + """Persist *document* to the outputs directory and return the file path.""" if name not in _CANONICAL_OUTPUTS: raise ValueError(f"Unsupported output name: {name}") - target = self.outputs_dir / name + target = context.outputs_dir / name target.parent.mkdir(parents=True, exist_ok=True) - content = _json_dumps(document) - target.write_text(content) - self._maybe_sign(name, document, content) + text = self._json_dumps(document) + target.write_text(text, encoding="utf-8") return target - def write_binary_output(self, name: str, blob: bytes) -> Path: - target = self.outputs_dir / name + def write_binary_output(self, context: RunContext, name: str, blob: bytes) -> Path: + target = context.outputs_dir / name target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(blob) return target - def load_input_json(self, name: str) -> Any: - path = self.inputs_dir / name - if not path.exists(): - raise FileNotFoundError(path) - return json.loads(path.read_text()) - - def load_output_json(self, name: str) -> Any: - path = self.outputs_dir / name - if not path.exists(): - raise FileNotFoundError(path) - return json.loads(path.read_text()) - - def relative_to_outputs(self, path: Path) -> str: - rel = Path("..") / path.relative_to(self.run_path) - return str(rel) - - def _maybe_sign(self, name: str, document: Mapping[str, Any] | Iterable[Any], content: str) -> None: - if not self.sign_outputs or not isinstance(document, Mapping): - return - try: - envelope = signing.sign_manifest(document) - except signing.SigningError: - return - signature_path = self.signed_outputs_dir / f"{name}.manifest.json" - signature_path.parent.mkdir(parents=True, exist_ok=True) - signature_path.write_text(_json_dumps(envelope)) - digest = envelope.get("digest", {}).get("sha256") + def write_signed_manifest( + self, context: RunContext, name: str, envelope: Mapping[str, Any] + ) -> Path: + target = context.signed_outputs_dir / f"{name}.manifest.json" + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(self._json_dumps(envelope), encoding="utf-8") + return target + + def append_transparency_index( + self, context: RunContext, canonical: str, digest: str, kid: str | None + ) -> Path: + context.transparency_index.parent.mkdir(parents=True, exist_ok=True) timestamp = _dt.datetime.utcnow().isoformat() + "Z" - line = f"{timestamp} {name} sha256={digest or hashlib.sha256(content.encode('utf-8')).hexdigest()} kid={envelope.get('kid') or 'unknown'}\n" - self.transparency_index.parent.mkdir(parents=True, exist_ok=True) - with self.transparency_index.open("a", encoding="utf-8") as handle: + line = f"TS={timestamp} FILE={canonical} SHA256={digest} KID={kid or 'unknown'}\n" + with context.transparency_index.open("a", encoding="utf-8") as handle: handle.write(line) + return context.transparency_index + + def list_runs(self, app_id: str | None) -> list[str]: + normalised = self._normalise_app_id(app_id) + app_root = self.root / normalised + if not app_root.exists(): + return [] + runs = [entry.name for entry in app_root.iterdir() if entry.is_dir()] + runs.sort() + return runs + + # ------------------------------------------------------------------ + # Internal helpers + def _make_context( + self, app_id: str | None, *, sign_outputs: bool = False + ) -> RunContext: + normalised = self._normalise_app_id(app_id) + timestamp = _dt.datetime.utcnow().strftime("%Y%m%d-%H%M%S") + run_id = timestamp + counter = 1 + while (self.root / normalised / run_id).exists(): + counter += 1 + run_id = f"{timestamp}-{counter:02d}" + return RunContext(app_id=normalised, run_id=run_id, root=self.root, sign_outputs=sign_outputs) + + def _prepare_directories(self, context: RunContext) -> None: + context.inputs_dir.mkdir(parents=True, exist_ok=True) + context.signed_outputs_dir.mkdir(parents=True, exist_ok=True) + + def _write_latest_marker(self, context: RunContext) -> None: + marker = self._latest_marker(context.app_id) + marker.parent.mkdir(parents=True, exist_ok=True) + payload = { + "run_id": context.run_id, + "updated_at": _dt.datetime.utcnow().isoformat() + "Z", + } + marker.write_text(self._json_dumps(payload), encoding="utf-8") + + def _latest_marker(self, app_id: str) -> Path: + return self.root / app_id / "LATEST" + + @staticmethod + def _json_dumps(data: Mapping[str, Any] | Iterable[Any]) -> str: + return json.dumps(data, indent=2, sort_keys=True, ensure_ascii=False) + + @staticmethod + def _normalise_app_id(app_id: str | None) -> str: + if not app_id: + return "APP-UNKNOWN" + candidate = app_id.strip() + if not candidate: + return "APP-UNKNOWN" + safe = [ch if ch.isalnum() or ch in {"-", "_"} else "-" for ch in candidate] + return "".join(safe) + + +_DEFAULT_REGISTRY = RunRegistry() def resolve_run(app_id: str | None, *, sign_outputs: bool = False) -> RunContext: - """Resolve or create the run context for the provided application identifier.""" - - normalised_app = _normalise_app_id(app_id) - run_id = _dt.datetime.utcnow().strftime("%Y%m%d-%H%M%S") - root = ARTEFACTS_ROOT - run_dir = root / normalised_app / run_id - _prepare_directories(run_dir) - return RunContext(app_id=normalised_app, run_id=run_id, root=root, sign_outputs=sign_outputs) - - -def _normalise_app_id(app_id: str | None) -> str: - if not app_id: - return "APP-UNKNOWN" - candidate = app_id.strip() or "APP-UNKNOWN" - safe = [ch if ch.isalnum() or ch in ("-", "_") else "-" for ch in candidate] - return "".join(safe) - - -def _json_dumps(data: Mapping[str, Any] | Iterable[Any]) -> str: - return json.dumps(data, indent=2, sort_keys=True, ensure_ascii=False) + return _DEFAULT_REGISTRY.create_run(app_id, sign_outputs=sign_outputs) def reopen_run(app_id: str | None, run_id: str, *, sign_outputs: bool = False) -> RunContext: - """Return a run context for an existing run identifier.""" - - normalised_app = _normalise_app_id(app_id) - root = ARTEFACTS_ROOT - run_dir = root / normalised_app / run_id - if not run_dir.exists(): - raise FileNotFoundError(run_dir) - _prepare_directories(run_dir) - return RunContext(app_id=normalised_app, run_id=run_id, root=root, sign_outputs=sign_outputs) + return _DEFAULT_REGISTRY.reopen_run(app_id, run_id, sign_outputs=sign_outputs) def list_runs(app_id: str | None) -> list[str]: - """Return available run identifiers for the provided application.""" + return _DEFAULT_REGISTRY.list_runs(app_id) - normalised_app = _normalise_app_id(app_id) - app_root = ARTEFACTS_ROOT / normalised_app - if not app_root.exists(): - return [] - runs = [entry.name for entry in app_root.iterdir() if entry.is_dir()] - runs.sort() - return runs +__all__ = ["RunRegistry", "RunContext", "resolve_run", "reopen_run", "list_runs"] -def _prepare_directories(run_dir: Path) -> None: - (run_dir / "inputs").mkdir(parents=True, exist_ok=True) - (run_dir / "outputs" / "signed").mkdir(parents=True, exist_ok=True) diff --git a/tests/test_api_artefacts.py b/tests/test_api_artefacts.py index ff88f8f6e..a8f83a9f8 100644 --- a/tests/test_api_artefacts.py +++ b/tests/test_api_artefacts.py @@ -3,74 +3,53 @@ import json from pathlib import Path +import importlib +import pytest from fastapi.testclient import TestClient -from fixops-blended-enterprise.src.main import create_app +REPO_ROOT = Path(__file__).resolve().parent.parent +SIM_ROOT = REPO_ROOT / "simulations" / "demo_pack" -APP_ID = "life-claims-portal" -SIM_DIR = Path("simulations/demo_pack") -EXPECTED_OUTPUTS = { - "requirements": "requirements.json", - "design": "design.manifest.json", - "build": "build.report.json", - "test": "test.report.json", - "deploy": "deploy.manifest.json", - "operate": "operate.snapshot.json", - "decision": "decision.json", -} +@pytest.fixture() +def api_client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> TestClient: + monkeypatch.setenv("FIXOPS_ARTEFACTS_ROOT", str(tmp_path)) + monkeypatch.setenv("FIXOPS_API_KEY", "test-key") + # Ensure settings pick up the patched environment + settings_module = importlib.import_module("src.config.settings") + importlib.reload(settings_module) + from src.main import create_app -def _client() -> TestClient: app = create_app() return TestClient(app) -def _post_stage(client: TestClient, stage: str, file_path: Path | None) -> dict[str, str]: - files = {} - if file_path is not None: - files["payload"] = (file_path.name, file_path.read_bytes()) +def _post_stage(client: TestClient, stage: str, input_name: str | None) -> dict[str, str | list[str] | bool | None]: + files = None + if input_name is not None: + path = SIM_ROOT / input_name + files = {"payload": (path.name, path.read_bytes(), "application/json")} response = client.post( "/api/v1/artefacts", - data={"type": stage, "app_name": APP_ID}, + data={"type": stage, "app_name": "life-claims-portal"}, files=files, - headers={"Authorization": "Bearer local-dev-key"}, + headers={"Authorization": "Bearer test-key"}, ) assert response.status_code == 201, response.text - payload = response.json() - outputs_dir = Path(payload["outputs_dir"]) - assert outputs_dir.exists() - expected = outputs_dir / EXPECTED_OUTPUTS[stage] - assert expected.exists() - if stage == "decision": - decision_payload = json.loads(expected.read_text()) - assert decision_payload.get("marketplace_recommendations") is not None - return payload + return response.json() -def test_ingest_all_stages_via_api(signing_env: None) -> None: - artefact_root = Path("artefacts") / APP_ID - if artefact_root.exists(): - for child in artefact_root.rglob("*"): - if child.is_file(): - child.unlink() - for child in sorted(artefact_root.rglob("*"), reverse=True): - if child.is_dir(): - child.rmdir() - artefact_root.rmdir() +def test_artefact_ingest_persists_outputs(api_client: TestClient, tmp_path: Path) -> None: + summary_requirements = _post_stage(api_client, "requirements", "requirements-input.csv") + req_output = Path(summary_requirements["output_file"]) + assert req_output.exists() + json.loads(req_output.read_text(encoding="utf-8")) + + summary_design = _post_stage(api_client, "design", "design-input.json") + design_output = Path(summary_design["output_file"]) + assert design_output.exists() + document = json.loads(design_output.read_text(encoding="utf-8")) + assert document.get("app_id", "").startswith("APP-") + assert summary_design["run_id"] == summary_requirements["run_id"] - with _client() as client: - run_ids: list[str] = [] - for stage, path in ( - ("requirements", SIM_DIR / "requirements-input.csv"), - ("design", SIM_DIR / "design-input.json"), - ("build", SIM_DIR / "sbom.json"), - ("test", SIM_DIR / "scanner.sarif"), - ("deploy", SIM_DIR / "tfplan.json"), - ("operate", SIM_DIR / "ops-telemetry.json"), - ("decision", None), - ): - payload = _post_stage(client, stage, path) - run_ids.append(payload["run_id"]) - # Ensure each stage created its own run folder - assert len(set(run_ids)) == len(run_ids) diff --git a/tests/test_cli_stage_run.py b/tests/test_cli_stage_run.py index b0cdcced2..622280fc9 100644 --- a/tests/test_cli_stage_run.py +++ b/tests/test_cli_stage_run.py @@ -1,7 +1,7 @@ from __future__ import annotations import json -import re +import os import subprocess import sys from pathlib import Path @@ -9,9 +9,20 @@ import pytest -APP_ID = "life-claims-portal" -SIM_DIR = Path("simulations/demo_pack") -EXPECTED_OUTPUTS = { +REPO_ROOT = Path(__file__).resolve().parent.parent +SIM_ROOT = REPO_ROOT / "simulations" / "demo_pack" + +STAGE_INPUTS = [ + ("requirements", "requirements-input.csv"), + ("design", "design-input.json"), + ("build", "sbom.json"), + ("test", "scanner.sarif"), + ("deploy", "tfplan.json"), + ("operate", "ops-telemetry.json"), + ("decision", None), +] + +CANONICAL_OUTPUTS = { "requirements": "requirements.json", "design": "design.manifest.json", "build": "build.report.json", @@ -22,34 +33,18 @@ } -def _stage_inputs() -> list[tuple[str, Path | None]]: - return [ - ("requirements", SIM_DIR / "requirements-input.csv"), - ("design", SIM_DIR / "design-input.json"), - ("build", SIM_DIR / "sbom.json"), - ("test", SIM_DIR / "scanner.sarif"), - ("deploy", SIM_DIR / "tfplan.json"), - ("operate", SIM_DIR / "ops-telemetry.json"), - ("decision", None), - ] - - -@pytest.fixture(autouse=True) -def clean_run_registry(tmp_path_factory: pytest.TempPathFactory) -> None: - artefacts_root = Path("artefacts") / APP_ID - if artefacts_root.exists(): - for run_dir in artefacts_root.iterdir(): - if run_dir.is_dir(): - for child in run_dir.rglob("*"): - if child.is_file(): - child.unlink() - for child in sorted(run_dir.rglob("*"), reverse=True): - if child.is_dir(): - child.rmdir() - run_dir.rmdir() +def _pythonpath_env(tmp_path: Path) -> dict[str, str]: + env = os.environ.copy() + entries = [str(REPO_ROOT), str(REPO_ROOT / "fixops-blended-enterprise")] + existing = env.get("PYTHONPATH") + if existing: + entries.append(existing) + env["PYTHONPATH"] = os.pathsep.join(entries) + env["FIXOPS_ARTEFACTS_ROOT"] = str(tmp_path) + return env -def _invoke_stage(stage: str, input_path: Path | None) -> tuple[str, Path]: +def _invoke_stage(stage: str, input_file: Path | None, env: dict[str, str]) -> None: cmd = [ sys.executable, "-m", @@ -58,28 +53,73 @@ def _invoke_stage(stage: str, input_path: Path | None) -> tuple[str, Path]: "--stage", stage, "--app", - APP_ID, + "life-claims-portal", ] - if input_path is not None: - cmd.extend(["--input", str(input_path)]) - completed = subprocess.run(cmd, capture_output=True, text=True, check=True) - match = re.search(r"run ([0-9T:-]+)", completed.stdout) - assert match, f"unable to parse run identifier from output: {completed.stdout}" - run_id = match.group(1) - outputs_dir = Path("artefacts") / APP_ID / run_id / "outputs" - assert outputs_dir.exists(), f"outputs directory missing for {stage}" - return run_id, outputs_dir - - -def test_stage_run_pipeline_generates_outputs(signing_env: None) -> None: - for stage, input_file in _stage_inputs(): - run_id, outputs_dir = _invoke_stage(stage, input_file) - expected = outputs_dir / EXPECTED_OUTPUTS[stage] - assert expected.exists(), f"missing canonical output for {stage}" - if stage == "design": - payload = json.loads(expected.read_text()) - assert payload.get("app_id") == APP_ID + if input_file is not None: + cmd.extend(["--input", str(input_file)]) + result = subprocess.run( + cmd, + cwd=REPO_ROOT, + env=env, + capture_output=True, + text=True, + ) + assert result.returncode == 0, f"stage {stage} failed: {result.stdout}\n{result.stderr}" + + +def _latest_run(tmp_path: Path) -> tuple[str, str, Path]: + app_dirs = [entry for entry in tmp_path.iterdir() if entry.is_dir() and entry.name.startswith("APP-")] + assert app_dirs, f"expected artefact directory under {tmp_path}" + app_root = app_dirs[0] + latest = json.loads((app_root / "LATEST").read_text(encoding="utf-8")) + run_id = latest["run_id"] + outputs_dir = app_root / run_id / "outputs" + return app_root.name, run_id, outputs_dir + + +@pytest.mark.integration +def test_stage_run_materialises_canonical_outputs(tmp_path: Path) -> None: + env = _pythonpath_env(tmp_path) + for stage, filename in STAGE_INPUTS: + input_path = SIM_ROOT / filename if filename else None + _invoke_stage(stage, input_path, env) + app_id, run_id, outputs_dir = _latest_run(tmp_path) + canonical_name = CANONICAL_OUTPUTS[stage] + target = outputs_dir / canonical_name + assert target.exists(), f"missing {canonical_name} for {stage}" + if target.suffix == ".json": + json.loads(target.read_text(encoding="utf-8")) if stage == "decision": - decision_payload = json.loads(expected.read_text()) - assert len(decision_payload.get("top_factors", [])) >= 2 - assert "compliance_rollup" in decision_payload + bundle = outputs_dir / "evidence_bundle.zip" + manifest = outputs_dir / "manifest.json" + assert bundle.exists() + assert manifest.exists() + json.loads(manifest.read_text(encoding="utf-8")) + + +def test_requirements_stage_starts_new_run(tmp_path: Path) -> None: + env = _pythonpath_env(tmp_path) + requirements_path = SIM_ROOT / "requirements-input.csv" + + _invoke_stage("requirements", requirements_path, env) + _app_id, first_run_id, _ = _latest_run(tmp_path) + + _invoke_stage("requirements", requirements_path, env) + _app_id, second_run_id, _ = _latest_run(tmp_path) + + assert second_run_id != first_run_id + + +def test_design_stage_reuses_current_run(tmp_path: Path) -> None: + env = _pythonpath_env(tmp_path) + requirements_path = SIM_ROOT / "requirements-input.csv" + design_path = SIM_ROOT / "design-input.json" + + _invoke_stage("requirements", requirements_path, env) + _app_id, first_run_id, _ = _latest_run(tmp_path) + + _invoke_stage("design", design_path, env) + _app_id, second_run_id, _ = _latest_run(tmp_path) + + assert second_run_id == first_run_id + diff --git a/tests/test_no_wip_imports.py b/tests/test_no_wip_imports.py new file mode 100644 index 000000000..9e267f9f5 --- /dev/null +++ b/tests/test_no_wip_imports.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from pathlib import Path + + +CHECK_DIRECTORIES = [ + Path("core"), + Path("apps"), + Path("fixops-blended-enterprise") / "src", +] + + +def test_no_wip_imports() -> None: + for directory in CHECK_DIRECTORIES: + for path in directory.rglob("*.py"): + text = path.read_text(encoding="utf-8") + assert "import WIP" not in text, f"disallowed import in {path}" + assert "from WIP" not in text, f"disallowed import in {path}" +