PraisonAI 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- praisonai/__init__.py +54 -0
- praisonai/__main__.py +15 -0
- praisonai/acp/__init__.py +54 -0
- praisonai/acp/config.py +159 -0
- praisonai/acp/server.py +587 -0
- praisonai/acp/session.py +219 -0
- praisonai/adapters/__init__.py +50 -0
- praisonai/adapters/readers.py +395 -0
- praisonai/adapters/rerankers.py +315 -0
- praisonai/adapters/retrievers.py +394 -0
- praisonai/adapters/vector_stores.py +409 -0
- praisonai/agent_scheduler.py +337 -0
- praisonai/agents_generator.py +903 -0
- praisonai/api/call.py +292 -0
- praisonai/auto.py +1197 -0
- praisonai/capabilities/__init__.py +275 -0
- praisonai/capabilities/a2a.py +140 -0
- praisonai/capabilities/assistants.py +283 -0
- praisonai/capabilities/audio.py +320 -0
- praisonai/capabilities/batches.py +469 -0
- praisonai/capabilities/completions.py +336 -0
- praisonai/capabilities/container_files.py +155 -0
- praisonai/capabilities/containers.py +93 -0
- praisonai/capabilities/embeddings.py +158 -0
- praisonai/capabilities/files.py +467 -0
- praisonai/capabilities/fine_tuning.py +293 -0
- praisonai/capabilities/guardrails.py +182 -0
- praisonai/capabilities/images.py +330 -0
- praisonai/capabilities/mcp.py +190 -0
- praisonai/capabilities/messages.py +270 -0
- praisonai/capabilities/moderations.py +154 -0
- praisonai/capabilities/ocr.py +217 -0
- praisonai/capabilities/passthrough.py +204 -0
- praisonai/capabilities/rag.py +207 -0
- praisonai/capabilities/realtime.py +160 -0
- praisonai/capabilities/rerank.py +165 -0
- praisonai/capabilities/responses.py +266 -0
- praisonai/capabilities/search.py +109 -0
- praisonai/capabilities/skills.py +133 -0
- praisonai/capabilities/vector_store_files.py +334 -0
- praisonai/capabilities/vector_stores.py +304 -0
- praisonai/capabilities/videos.py +141 -0
- praisonai/chainlit_ui.py +304 -0
- praisonai/chat/__init__.py +106 -0
- praisonai/chat/app.py +125 -0
- praisonai/cli/__init__.py +26 -0
- praisonai/cli/app.py +213 -0
- praisonai/cli/commands/__init__.py +75 -0
- praisonai/cli/commands/acp.py +70 -0
- praisonai/cli/commands/completion.py +333 -0
- praisonai/cli/commands/config.py +166 -0
- praisonai/cli/commands/debug.py +142 -0
- praisonai/cli/commands/diag.py +55 -0
- praisonai/cli/commands/doctor.py +166 -0
- praisonai/cli/commands/environment.py +179 -0
- praisonai/cli/commands/lsp.py +112 -0
- praisonai/cli/commands/mcp.py +210 -0
- praisonai/cli/commands/profile.py +457 -0
- praisonai/cli/commands/run.py +228 -0
- praisonai/cli/commands/schedule.py +150 -0
- praisonai/cli/commands/serve.py +97 -0
- praisonai/cli/commands/session.py +212 -0
- praisonai/cli/commands/traces.py +145 -0
- praisonai/cli/commands/version.py +101 -0
- praisonai/cli/configuration/__init__.py +18 -0
- praisonai/cli/configuration/loader.py +353 -0
- praisonai/cli/configuration/paths.py +114 -0
- praisonai/cli/configuration/schema.py +164 -0
- praisonai/cli/features/__init__.py +268 -0
- praisonai/cli/features/acp.py +236 -0
- praisonai/cli/features/action_orchestrator.py +546 -0
- praisonai/cli/features/agent_scheduler.py +773 -0
- praisonai/cli/features/agent_tools.py +474 -0
- praisonai/cli/features/agents.py +375 -0
- praisonai/cli/features/at_mentions.py +471 -0
- praisonai/cli/features/auto_memory.py +182 -0
- praisonai/cli/features/autonomy_mode.py +490 -0
- praisonai/cli/features/background.py +356 -0
- praisonai/cli/features/base.py +168 -0
- praisonai/cli/features/capabilities.py +1326 -0
- praisonai/cli/features/checkpoints.py +338 -0
- praisonai/cli/features/code_intelligence.py +652 -0
- praisonai/cli/features/compaction.py +294 -0
- praisonai/cli/features/compare.py +534 -0
- praisonai/cli/features/cost_tracker.py +514 -0
- praisonai/cli/features/debug.py +810 -0
- praisonai/cli/features/deploy.py +517 -0
- praisonai/cli/features/diag.py +289 -0
- praisonai/cli/features/doctor/__init__.py +63 -0
- praisonai/cli/features/doctor/checks/__init__.py +24 -0
- praisonai/cli/features/doctor/checks/acp_checks.py +240 -0
- praisonai/cli/features/doctor/checks/config_checks.py +366 -0
- praisonai/cli/features/doctor/checks/db_checks.py +366 -0
- praisonai/cli/features/doctor/checks/env_checks.py +543 -0
- praisonai/cli/features/doctor/checks/lsp_checks.py +199 -0
- praisonai/cli/features/doctor/checks/mcp_checks.py +349 -0
- praisonai/cli/features/doctor/checks/memory_checks.py +268 -0
- praisonai/cli/features/doctor/checks/network_checks.py +251 -0
- praisonai/cli/features/doctor/checks/obs_checks.py +328 -0
- praisonai/cli/features/doctor/checks/performance_checks.py +235 -0
- praisonai/cli/features/doctor/checks/permissions_checks.py +259 -0
- praisonai/cli/features/doctor/checks/selftest_checks.py +322 -0
- praisonai/cli/features/doctor/checks/serve_checks.py +426 -0
- praisonai/cli/features/doctor/checks/skills_checks.py +231 -0
- praisonai/cli/features/doctor/checks/tools_checks.py +371 -0
- praisonai/cli/features/doctor/engine.py +266 -0
- praisonai/cli/features/doctor/formatters.py +310 -0
- praisonai/cli/features/doctor/handler.py +397 -0
- praisonai/cli/features/doctor/models.py +264 -0
- praisonai/cli/features/doctor/registry.py +239 -0
- praisonai/cli/features/endpoints.py +1019 -0
- praisonai/cli/features/eval.py +560 -0
- praisonai/cli/features/external_agents.py +231 -0
- praisonai/cli/features/fast_context.py +410 -0
- praisonai/cli/features/flow_display.py +566 -0
- praisonai/cli/features/git_integration.py +651 -0
- praisonai/cli/features/guardrail.py +171 -0
- praisonai/cli/features/handoff.py +185 -0
- praisonai/cli/features/hooks.py +583 -0
- praisonai/cli/features/image.py +384 -0
- praisonai/cli/features/interactive_runtime.py +585 -0
- praisonai/cli/features/interactive_tools.py +380 -0
- praisonai/cli/features/interactive_tui.py +603 -0
- praisonai/cli/features/jobs.py +632 -0
- praisonai/cli/features/knowledge.py +531 -0
- praisonai/cli/features/lite.py +244 -0
- praisonai/cli/features/lsp_cli.py +225 -0
- praisonai/cli/features/mcp.py +169 -0
- praisonai/cli/features/message_queue.py +587 -0
- praisonai/cli/features/metrics.py +211 -0
- praisonai/cli/features/n8n.py +673 -0
- praisonai/cli/features/observability.py +293 -0
- praisonai/cli/features/ollama.py +361 -0
- praisonai/cli/features/output_style.py +273 -0
- praisonai/cli/features/package.py +631 -0
- praisonai/cli/features/performance.py +308 -0
- praisonai/cli/features/persistence.py +636 -0
- praisonai/cli/features/profile.py +226 -0
- praisonai/cli/features/profiler/__init__.py +81 -0
- praisonai/cli/features/profiler/core.py +558 -0
- praisonai/cli/features/profiler/optimizations.py +652 -0
- praisonai/cli/features/profiler/suite.py +386 -0
- praisonai/cli/features/profiling.py +350 -0
- praisonai/cli/features/queue/__init__.py +73 -0
- praisonai/cli/features/queue/manager.py +395 -0
- praisonai/cli/features/queue/models.py +286 -0
- praisonai/cli/features/queue/persistence.py +564 -0
- praisonai/cli/features/queue/scheduler.py +484 -0
- praisonai/cli/features/queue/worker.py +372 -0
- praisonai/cli/features/recipe.py +1723 -0
- praisonai/cli/features/recipes.py +449 -0
- praisonai/cli/features/registry.py +229 -0
- praisonai/cli/features/repo_map.py +860 -0
- praisonai/cli/features/router.py +466 -0
- praisonai/cli/features/sandbox_executor.py +515 -0
- praisonai/cli/features/serve.py +829 -0
- praisonai/cli/features/session.py +222 -0
- praisonai/cli/features/skills.py +856 -0
- praisonai/cli/features/slash_commands.py +650 -0
- praisonai/cli/features/telemetry.py +179 -0
- praisonai/cli/features/templates.py +1384 -0
- praisonai/cli/features/thinking.py +305 -0
- praisonai/cli/features/todo.py +334 -0
- praisonai/cli/features/tools.py +680 -0
- praisonai/cli/features/tui/__init__.py +83 -0
- praisonai/cli/features/tui/app.py +580 -0
- praisonai/cli/features/tui/cli.py +566 -0
- praisonai/cli/features/tui/debug.py +511 -0
- praisonai/cli/features/tui/events.py +99 -0
- praisonai/cli/features/tui/mock_provider.py +328 -0
- praisonai/cli/features/tui/orchestrator.py +652 -0
- praisonai/cli/features/tui/screens/__init__.py +50 -0
- praisonai/cli/features/tui/screens/main.py +245 -0
- praisonai/cli/features/tui/screens/queue.py +174 -0
- praisonai/cli/features/tui/screens/session.py +124 -0
- praisonai/cli/features/tui/screens/settings.py +148 -0
- praisonai/cli/features/tui/widgets/__init__.py +56 -0
- praisonai/cli/features/tui/widgets/chat.py +261 -0
- praisonai/cli/features/tui/widgets/composer.py +224 -0
- praisonai/cli/features/tui/widgets/queue_panel.py +200 -0
- praisonai/cli/features/tui/widgets/status.py +167 -0
- praisonai/cli/features/tui/widgets/tool_panel.py +248 -0
- praisonai/cli/features/workflow.py +720 -0
- praisonai/cli/legacy.py +236 -0
- praisonai/cli/main.py +5559 -0
- praisonai/cli/schedule_cli.py +54 -0
- praisonai/cli/state/__init__.py +31 -0
- praisonai/cli/state/identifiers.py +161 -0
- praisonai/cli/state/sessions.py +313 -0
- praisonai/code/__init__.py +93 -0
- praisonai/code/agent_tools.py +344 -0
- praisonai/code/diff/__init__.py +21 -0
- praisonai/code/diff/diff_strategy.py +432 -0
- praisonai/code/tools/__init__.py +27 -0
- praisonai/code/tools/apply_diff.py +221 -0
- praisonai/code/tools/execute_command.py +275 -0
- praisonai/code/tools/list_files.py +274 -0
- praisonai/code/tools/read_file.py +206 -0
- praisonai/code/tools/search_replace.py +248 -0
- praisonai/code/tools/write_file.py +217 -0
- praisonai/code/utils/__init__.py +46 -0
- praisonai/code/utils/file_utils.py +307 -0
- praisonai/code/utils/ignore_utils.py +308 -0
- praisonai/code/utils/text_utils.py +276 -0
- praisonai/db/__init__.py +64 -0
- praisonai/db/adapter.py +531 -0
- praisonai/deploy/__init__.py +62 -0
- praisonai/deploy/api.py +231 -0
- praisonai/deploy/docker.py +454 -0
- praisonai/deploy/doctor.py +367 -0
- praisonai/deploy/main.py +327 -0
- praisonai/deploy/models.py +179 -0
- praisonai/deploy/providers/__init__.py +33 -0
- praisonai/deploy/providers/aws.py +331 -0
- praisonai/deploy/providers/azure.py +358 -0
- praisonai/deploy/providers/base.py +101 -0
- praisonai/deploy/providers/gcp.py +314 -0
- praisonai/deploy/schema.py +208 -0
- praisonai/deploy.py +185 -0
- praisonai/endpoints/__init__.py +53 -0
- praisonai/endpoints/a2u_server.py +410 -0
- praisonai/endpoints/discovery.py +165 -0
- praisonai/endpoints/providers/__init__.py +28 -0
- praisonai/endpoints/providers/a2a.py +253 -0
- praisonai/endpoints/providers/a2u.py +208 -0
- praisonai/endpoints/providers/agents_api.py +171 -0
- praisonai/endpoints/providers/base.py +231 -0
- praisonai/endpoints/providers/mcp.py +263 -0
- praisonai/endpoints/providers/recipe.py +206 -0
- praisonai/endpoints/providers/tools_mcp.py +150 -0
- praisonai/endpoints/registry.py +131 -0
- praisonai/endpoints/server.py +161 -0
- praisonai/inbuilt_tools/__init__.py +24 -0
- praisonai/inbuilt_tools/autogen_tools.py +117 -0
- praisonai/inc/__init__.py +2 -0
- praisonai/inc/config.py +96 -0
- praisonai/inc/models.py +155 -0
- praisonai/integrations/__init__.py +56 -0
- praisonai/integrations/base.py +303 -0
- praisonai/integrations/claude_code.py +270 -0
- praisonai/integrations/codex_cli.py +255 -0
- praisonai/integrations/cursor_cli.py +195 -0
- praisonai/integrations/gemini_cli.py +222 -0
- praisonai/jobs/__init__.py +67 -0
- praisonai/jobs/executor.py +425 -0
- praisonai/jobs/models.py +230 -0
- praisonai/jobs/router.py +314 -0
- praisonai/jobs/server.py +186 -0
- praisonai/jobs/store.py +203 -0
- praisonai/llm/__init__.py +66 -0
- praisonai/llm/registry.py +382 -0
- praisonai/mcp_server/__init__.py +152 -0
- praisonai/mcp_server/adapters/__init__.py +74 -0
- praisonai/mcp_server/adapters/agents.py +128 -0
- praisonai/mcp_server/adapters/capabilities.py +168 -0
- praisonai/mcp_server/adapters/cli_tools.py +568 -0
- praisonai/mcp_server/adapters/extended_capabilities.py +462 -0
- praisonai/mcp_server/adapters/knowledge.py +93 -0
- praisonai/mcp_server/adapters/memory.py +104 -0
- praisonai/mcp_server/adapters/prompts.py +306 -0
- praisonai/mcp_server/adapters/resources.py +124 -0
- praisonai/mcp_server/adapters/tools_bridge.py +280 -0
- praisonai/mcp_server/auth/__init__.py +48 -0
- praisonai/mcp_server/auth/api_key.py +291 -0
- praisonai/mcp_server/auth/oauth.py +460 -0
- praisonai/mcp_server/auth/oidc.py +289 -0
- praisonai/mcp_server/auth/scopes.py +260 -0
- praisonai/mcp_server/cli.py +852 -0
- praisonai/mcp_server/elicitation.py +445 -0
- praisonai/mcp_server/icons.py +302 -0
- praisonai/mcp_server/recipe_adapter.py +573 -0
- praisonai/mcp_server/recipe_cli.py +824 -0
- praisonai/mcp_server/registry.py +703 -0
- praisonai/mcp_server/sampling.py +422 -0
- praisonai/mcp_server/server.py +490 -0
- praisonai/mcp_server/tasks.py +443 -0
- praisonai/mcp_server/transports/__init__.py +18 -0
- praisonai/mcp_server/transports/http_stream.py +376 -0
- praisonai/mcp_server/transports/stdio.py +132 -0
- praisonai/persistence/__init__.py +84 -0
- praisonai/persistence/config.py +238 -0
- praisonai/persistence/conversation/__init__.py +25 -0
- praisonai/persistence/conversation/async_mysql.py +427 -0
- praisonai/persistence/conversation/async_postgres.py +410 -0
- praisonai/persistence/conversation/async_sqlite.py +371 -0
- praisonai/persistence/conversation/base.py +151 -0
- praisonai/persistence/conversation/json_store.py +250 -0
- praisonai/persistence/conversation/mysql.py +387 -0
- praisonai/persistence/conversation/postgres.py +401 -0
- praisonai/persistence/conversation/singlestore.py +240 -0
- praisonai/persistence/conversation/sqlite.py +341 -0
- praisonai/persistence/conversation/supabase.py +203 -0
- praisonai/persistence/conversation/surrealdb.py +287 -0
- praisonai/persistence/factory.py +301 -0
- praisonai/persistence/hooks/__init__.py +18 -0
- praisonai/persistence/hooks/agent_hooks.py +297 -0
- praisonai/persistence/knowledge/__init__.py +26 -0
- praisonai/persistence/knowledge/base.py +144 -0
- praisonai/persistence/knowledge/cassandra.py +232 -0
- praisonai/persistence/knowledge/chroma.py +295 -0
- praisonai/persistence/knowledge/clickhouse.py +242 -0
- praisonai/persistence/knowledge/cosmosdb_vector.py +438 -0
- praisonai/persistence/knowledge/couchbase.py +286 -0
- praisonai/persistence/knowledge/lancedb.py +216 -0
- praisonai/persistence/knowledge/langchain_adapter.py +291 -0
- praisonai/persistence/knowledge/lightrag_adapter.py +212 -0
- praisonai/persistence/knowledge/llamaindex_adapter.py +256 -0
- praisonai/persistence/knowledge/milvus.py +277 -0
- praisonai/persistence/knowledge/mongodb_vector.py +306 -0
- praisonai/persistence/knowledge/pgvector.py +335 -0
- praisonai/persistence/knowledge/pinecone.py +253 -0
- praisonai/persistence/knowledge/qdrant.py +301 -0
- praisonai/persistence/knowledge/redis_vector.py +291 -0
- praisonai/persistence/knowledge/singlestore_vector.py +299 -0
- praisonai/persistence/knowledge/surrealdb_vector.py +309 -0
- praisonai/persistence/knowledge/upstash_vector.py +266 -0
- praisonai/persistence/knowledge/weaviate.py +223 -0
- praisonai/persistence/migrations/__init__.py +10 -0
- praisonai/persistence/migrations/manager.py +251 -0
- praisonai/persistence/orchestrator.py +406 -0
- praisonai/persistence/state/__init__.py +21 -0
- praisonai/persistence/state/async_mongodb.py +200 -0
- praisonai/persistence/state/base.py +107 -0
- praisonai/persistence/state/dynamodb.py +226 -0
- praisonai/persistence/state/firestore.py +175 -0
- praisonai/persistence/state/gcs.py +155 -0
- praisonai/persistence/state/memory.py +245 -0
- praisonai/persistence/state/mongodb.py +158 -0
- praisonai/persistence/state/redis.py +190 -0
- praisonai/persistence/state/upstash.py +144 -0
- praisonai/persistence/tests/__init__.py +3 -0
- praisonai/persistence/tests/test_all_backends.py +633 -0
- praisonai/profiler.py +1214 -0
- praisonai/recipe/__init__.py +134 -0
- praisonai/recipe/bridge.py +278 -0
- praisonai/recipe/core.py +893 -0
- praisonai/recipe/exceptions.py +54 -0
- praisonai/recipe/history.py +402 -0
- praisonai/recipe/models.py +266 -0
- praisonai/recipe/operations.py +440 -0
- praisonai/recipe/policy.py +422 -0
- praisonai/recipe/registry.py +849 -0
- praisonai/recipe/runtime.py +214 -0
- praisonai/recipe/security.py +711 -0
- praisonai/recipe/serve.py +859 -0
- praisonai/recipe/server.py +613 -0
- praisonai/scheduler/__init__.py +45 -0
- praisonai/scheduler/agent_scheduler.py +552 -0
- praisonai/scheduler/base.py +124 -0
- praisonai/scheduler/daemon_manager.py +225 -0
- praisonai/scheduler/state_manager.py +155 -0
- praisonai/scheduler/yaml_loader.py +193 -0
- praisonai/scheduler.py +194 -0
- praisonai/setup/__init__.py +1 -0
- praisonai/setup/build.py +21 -0
- praisonai/setup/post_install.py +23 -0
- praisonai/setup/setup_conda_env.py +25 -0
- praisonai/setup.py +16 -0
- praisonai/templates/__init__.py +116 -0
- praisonai/templates/cache.py +364 -0
- praisonai/templates/dependency_checker.py +358 -0
- praisonai/templates/discovery.py +391 -0
- praisonai/templates/loader.py +564 -0
- praisonai/templates/registry.py +511 -0
- praisonai/templates/resolver.py +206 -0
- praisonai/templates/security.py +327 -0
- praisonai/templates/tool_override.py +498 -0
- praisonai/templates/tools_doctor.py +256 -0
- praisonai/test.py +105 -0
- praisonai/train.py +562 -0
- praisonai/train_vision.py +306 -0
- praisonai/ui/agents.py +824 -0
- praisonai/ui/callbacks.py +57 -0
- praisonai/ui/chainlit_compat.py +246 -0
- praisonai/ui/chat.py +532 -0
- praisonai/ui/code.py +717 -0
- praisonai/ui/colab.py +474 -0
- praisonai/ui/colab_chainlit.py +81 -0
- praisonai/ui/components/aicoder.py +284 -0
- praisonai/ui/context.py +283 -0
- praisonai/ui/database_config.py +56 -0
- praisonai/ui/db.py +294 -0
- praisonai/ui/realtime.py +488 -0
- praisonai/ui/realtimeclient/__init__.py +756 -0
- praisonai/ui/realtimeclient/tools.py +242 -0
- praisonai/ui/sql_alchemy.py +710 -0
- praisonai/upload_vision.py +140 -0
- praisonai/version.py +1 -0
- praisonai-3.0.0.dist-info/METADATA +3493 -0
- praisonai-3.0.0.dist-info/RECORD +393 -0
- praisonai-3.0.0.dist-info/WHEEL +5 -0
- praisonai-3.0.0.dist-info/entry_points.txt +4 -0
- praisonai-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recipe Security Module
|
|
3
|
+
|
|
4
|
+
Provides security features for recipes:
|
|
5
|
+
- SBOM (Software Bill of Materials) generation
|
|
6
|
+
- Bundle signing and verification
|
|
7
|
+
- Dependency auditing
|
|
8
|
+
- PII redaction
|
|
9
|
+
- Lockfile validation
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SecurityError(Exception):
|
|
23
|
+
"""Base exception for security operations."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SignatureError(SecurityError):
|
|
28
|
+
"""Signature verification failed."""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LockfileError(SecurityError):
|
|
33
|
+
"""Lockfile validation failed."""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get_timestamp() -> str:
|
|
38
|
+
"""Get current timestamp in ISO format."""
|
|
39
|
+
return datetime.now(timezone.utc).isoformat()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ============================================================================
|
|
43
|
+
# SBOM Generation
|
|
44
|
+
# ============================================================================
|
|
45
|
+
|
|
46
|
+
def generate_sbom(
|
|
47
|
+
recipe_path: Union[str, Path],
|
|
48
|
+
format: str = "cyclonedx",
|
|
49
|
+
include_python_deps: bool = True,
|
|
50
|
+
include_tools: bool = True,
|
|
51
|
+
) -> Dict[str, Any]:
|
|
52
|
+
"""
|
|
53
|
+
Generate Software Bill of Materials for a recipe.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
recipe_path: Path to recipe directory or bundle
|
|
57
|
+
format: Output format (cyclonedx or spdx)
|
|
58
|
+
include_python_deps: Include Python dependencies
|
|
59
|
+
include_tools: Include tool dependencies
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
SBOM as dictionary
|
|
63
|
+
"""
|
|
64
|
+
recipe_path = Path(recipe_path)
|
|
65
|
+
|
|
66
|
+
# Load recipe manifest
|
|
67
|
+
template_path = recipe_path / "TEMPLATE.yaml"
|
|
68
|
+
manifest = {}
|
|
69
|
+
if template_path.exists():
|
|
70
|
+
import yaml
|
|
71
|
+
with open(template_path) as f:
|
|
72
|
+
manifest = yaml.safe_load(f) or {}
|
|
73
|
+
|
|
74
|
+
# Get recipe info
|
|
75
|
+
recipe_name = manifest.get("name", recipe_path.name)
|
|
76
|
+
recipe_version = manifest.get("version", "0.0.0")
|
|
77
|
+
|
|
78
|
+
# Collect components
|
|
79
|
+
components = []
|
|
80
|
+
|
|
81
|
+
# Add praisonai as component
|
|
82
|
+
try:
|
|
83
|
+
import praisonai
|
|
84
|
+
praisonai_version = getattr(praisonai, "__version__", "unknown")
|
|
85
|
+
except ImportError:
|
|
86
|
+
praisonai_version = "unknown"
|
|
87
|
+
|
|
88
|
+
components.append({
|
|
89
|
+
"type": "library",
|
|
90
|
+
"name": "praisonai",
|
|
91
|
+
"version": praisonai_version,
|
|
92
|
+
"purl": f"pkg:pypi/praisonai@{praisonai_version}",
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
# Add Python dependencies from lockfile
|
|
96
|
+
if include_python_deps:
|
|
97
|
+
deps = _get_python_deps(recipe_path)
|
|
98
|
+
for dep in deps:
|
|
99
|
+
components.append({
|
|
100
|
+
"type": "library",
|
|
101
|
+
"name": dep["name"],
|
|
102
|
+
"version": dep["version"],
|
|
103
|
+
"purl": f"pkg:pypi/{dep['name']}@{dep['version']}",
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
# Add tool dependencies
|
|
107
|
+
if include_tools:
|
|
108
|
+
requires = manifest.get("requires", {})
|
|
109
|
+
tools = requires.get("tools", [])
|
|
110
|
+
for tool in tools:
|
|
111
|
+
if isinstance(tool, str):
|
|
112
|
+
components.append({
|
|
113
|
+
"type": "application",
|
|
114
|
+
"name": tool,
|
|
115
|
+
"version": "unknown",
|
|
116
|
+
})
|
|
117
|
+
elif isinstance(tool, dict):
|
|
118
|
+
components.append({
|
|
119
|
+
"type": "application",
|
|
120
|
+
"name": tool.get("name", "unknown"),
|
|
121
|
+
"version": tool.get("version", "unknown"),
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
# External dependencies
|
|
125
|
+
external = requires.get("external", [])
|
|
126
|
+
for ext in external:
|
|
127
|
+
if isinstance(ext, str):
|
|
128
|
+
components.append({
|
|
129
|
+
"type": "application",
|
|
130
|
+
"name": ext,
|
|
131
|
+
"version": "unknown",
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
if format == "cyclonedx":
|
|
135
|
+
return _generate_cyclonedx(recipe_name, recipe_version, components)
|
|
136
|
+
elif format == "spdx":
|
|
137
|
+
return _generate_spdx(recipe_name, recipe_version, components)
|
|
138
|
+
else:
|
|
139
|
+
raise SecurityError(f"Unknown SBOM format: {format}")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _generate_cyclonedx(
|
|
143
|
+
name: str,
|
|
144
|
+
version: str,
|
|
145
|
+
components: List[Dict[str, Any]],
|
|
146
|
+
) -> Dict[str, Any]:
|
|
147
|
+
"""Generate CycloneDX SBOM."""
|
|
148
|
+
return {
|
|
149
|
+
"bomFormat": "CycloneDX",
|
|
150
|
+
"specVersion": "1.4",
|
|
151
|
+
"serialNumber": f"urn:uuid:{hashlib.md5(f'{name}{version}{_get_timestamp()}'.encode()).hexdigest()}",
|
|
152
|
+
"version": 1,
|
|
153
|
+
"metadata": {
|
|
154
|
+
"timestamp": _get_timestamp(),
|
|
155
|
+
"tools": [{"name": "praisonai-sbom", "version": "1.0.0"}],
|
|
156
|
+
"component": {
|
|
157
|
+
"type": "application",
|
|
158
|
+
"name": name,
|
|
159
|
+
"version": version,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
"components": [
|
|
163
|
+
{
|
|
164
|
+
"type": comp["type"],
|
|
165
|
+
"name": comp["name"],
|
|
166
|
+
"version": comp["version"],
|
|
167
|
+
"purl": comp.get("purl"),
|
|
168
|
+
}
|
|
169
|
+
for comp in components
|
|
170
|
+
],
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _generate_spdx(
|
|
175
|
+
name: str,
|
|
176
|
+
version: str,
|
|
177
|
+
components: List[Dict[str, Any]],
|
|
178
|
+
) -> Dict[str, Any]:
|
|
179
|
+
"""Generate SPDX SBOM."""
|
|
180
|
+
return {
|
|
181
|
+
"spdxVersion": "SPDX-2.3",
|
|
182
|
+
"dataLicense": "CC0-1.0",
|
|
183
|
+
"SPDXID": "SPDXRef-DOCUMENT",
|
|
184
|
+
"name": f"{name}-{version}",
|
|
185
|
+
"documentNamespace": f"https://praison.ai/sbom/{name}/{version}",
|
|
186
|
+
"creationInfo": {
|
|
187
|
+
"created": _get_timestamp(),
|
|
188
|
+
"creators": ["Tool: praisonai-sbom-1.0.0"],
|
|
189
|
+
},
|
|
190
|
+
"packages": [
|
|
191
|
+
{
|
|
192
|
+
"SPDXID": f"SPDXRef-Package-{i}",
|
|
193
|
+
"name": comp["name"],
|
|
194
|
+
"versionInfo": comp["version"],
|
|
195
|
+
"downloadLocation": "NOASSERTION",
|
|
196
|
+
}
|
|
197
|
+
for i, comp in enumerate(components)
|
|
198
|
+
],
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _get_python_deps(recipe_path: Path) -> List[Dict[str, str]]:
|
|
203
|
+
"""Get Python dependencies from lockfile."""
|
|
204
|
+
deps = []
|
|
205
|
+
lock_dir = recipe_path / "lock"
|
|
206
|
+
|
|
207
|
+
# Try uv.lock
|
|
208
|
+
uv_lock = lock_dir / "uv.lock"
|
|
209
|
+
if uv_lock.exists():
|
|
210
|
+
deps.extend(_parse_uv_lock(uv_lock))
|
|
211
|
+
return deps
|
|
212
|
+
|
|
213
|
+
# Try requirements.lock
|
|
214
|
+
req_lock = lock_dir / "requirements.lock"
|
|
215
|
+
if req_lock.exists():
|
|
216
|
+
deps.extend(_parse_requirements_lock(req_lock))
|
|
217
|
+
return deps
|
|
218
|
+
|
|
219
|
+
# Try poetry.lock
|
|
220
|
+
poetry_lock = lock_dir / "poetry.lock"
|
|
221
|
+
if poetry_lock.exists():
|
|
222
|
+
deps.extend(_parse_poetry_lock(poetry_lock))
|
|
223
|
+
return deps
|
|
224
|
+
|
|
225
|
+
# Fallback: try requirements.txt in recipe root
|
|
226
|
+
req_txt = recipe_path / "requirements.txt"
|
|
227
|
+
if req_txt.exists():
|
|
228
|
+
deps.extend(_parse_requirements_lock(req_txt))
|
|
229
|
+
|
|
230
|
+
return deps
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _parse_uv_lock(path: Path) -> List[Dict[str, str]]:
|
|
234
|
+
"""Parse uv.lock file."""
|
|
235
|
+
deps = []
|
|
236
|
+
try:
|
|
237
|
+
import tomllib
|
|
238
|
+
except ImportError:
|
|
239
|
+
import tomli as tomllib
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
with open(path, "rb") as f:
|
|
243
|
+
data = tomllib.load(f)
|
|
244
|
+
|
|
245
|
+
for pkg in data.get("package", []):
|
|
246
|
+
deps.append({
|
|
247
|
+
"name": pkg.get("name", ""),
|
|
248
|
+
"version": pkg.get("version", ""),
|
|
249
|
+
})
|
|
250
|
+
except Exception:
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
return deps
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _parse_requirements_lock(path: Path) -> List[Dict[str, str]]:
|
|
257
|
+
"""Parse requirements.lock or requirements.txt."""
|
|
258
|
+
deps = []
|
|
259
|
+
|
|
260
|
+
with open(path) as f:
|
|
261
|
+
for line in f:
|
|
262
|
+
line = line.strip()
|
|
263
|
+
if not line or line.startswith("#") or line.startswith("-"):
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
# Parse package==version
|
|
267
|
+
match = re.match(r"^([a-zA-Z0-9_-]+)==([^\s;]+)", line)
|
|
268
|
+
if match:
|
|
269
|
+
deps.append({
|
|
270
|
+
"name": match.group(1),
|
|
271
|
+
"version": match.group(2),
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
return deps
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _parse_poetry_lock(path: Path) -> List[Dict[str, str]]:
|
|
278
|
+
"""Parse poetry.lock file."""
|
|
279
|
+
deps = []
|
|
280
|
+
try:
|
|
281
|
+
import tomllib
|
|
282
|
+
except ImportError:
|
|
283
|
+
import tomli as tomllib
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
with open(path, "rb") as f:
|
|
287
|
+
data = tomllib.load(f)
|
|
288
|
+
|
|
289
|
+
for pkg in data.get("package", []):
|
|
290
|
+
deps.append({
|
|
291
|
+
"name": pkg.get("name", ""),
|
|
292
|
+
"version": pkg.get("version", ""),
|
|
293
|
+
})
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
|
|
297
|
+
return deps
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# ============================================================================
|
|
301
|
+
# Bundle Signing
|
|
302
|
+
# ============================================================================
|
|
303
|
+
|
|
304
|
+
def sign_bundle(
|
|
305
|
+
bundle_path: Union[str, Path],
|
|
306
|
+
private_key_path: Union[str, Path],
|
|
307
|
+
output_path: Optional[Union[str, Path]] = None,
|
|
308
|
+
) -> Path:
|
|
309
|
+
"""
|
|
310
|
+
Sign a recipe bundle.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
bundle_path: Path to .praison bundle
|
|
314
|
+
private_key_path: Path to private key (PEM format)
|
|
315
|
+
output_path: Output path for signature (default: bundle.sig)
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Path to signature file
|
|
319
|
+
"""
|
|
320
|
+
bundle_path = Path(bundle_path)
|
|
321
|
+
private_key_path = Path(private_key_path)
|
|
322
|
+
|
|
323
|
+
if not bundle_path.exists():
|
|
324
|
+
raise SecurityError(f"Bundle not found: {bundle_path}")
|
|
325
|
+
if not private_key_path.exists():
|
|
326
|
+
raise SecurityError(f"Private key not found: {private_key_path}")
|
|
327
|
+
|
|
328
|
+
# Calculate bundle hash
|
|
329
|
+
bundle_hash = _calculate_file_hash(bundle_path)
|
|
330
|
+
|
|
331
|
+
# Sign using cryptography library (lazy import)
|
|
332
|
+
try:
|
|
333
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
334
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
335
|
+
except ImportError:
|
|
336
|
+
raise SecurityError("cryptography package required for signing. Install with: pip install cryptography")
|
|
337
|
+
|
|
338
|
+
# Load private key
|
|
339
|
+
with open(private_key_path, "rb") as f:
|
|
340
|
+
private_key = serialization.load_pem_private_key(f.read(), password=None)
|
|
341
|
+
|
|
342
|
+
# Sign the hash
|
|
343
|
+
signature = private_key.sign(
|
|
344
|
+
bundle_hash.encode(),
|
|
345
|
+
padding.PKCS1v15(),
|
|
346
|
+
hashes.SHA256(),
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Write signature
|
|
350
|
+
output_path = Path(output_path) if output_path else bundle_path.with_suffix(".praison.sig")
|
|
351
|
+
|
|
352
|
+
sig_data = {
|
|
353
|
+
"bundle": bundle_path.name,
|
|
354
|
+
"hash": bundle_hash,
|
|
355
|
+
"algorithm": "RSA-PKCS1v15-SHA256",
|
|
356
|
+
"signature": signature.hex(),
|
|
357
|
+
"signed_at": _get_timestamp(),
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
with open(output_path, "w") as f:
|
|
361
|
+
json.dump(sig_data, f, indent=2)
|
|
362
|
+
|
|
363
|
+
return output_path
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def verify_bundle(
|
|
367
|
+
bundle_path: Union[str, Path],
|
|
368
|
+
public_key_path: Union[str, Path],
|
|
369
|
+
signature_path: Optional[Union[str, Path]] = None,
|
|
370
|
+
) -> Tuple[bool, str]:
|
|
371
|
+
"""
|
|
372
|
+
Verify a signed recipe bundle.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
bundle_path: Path to .praison bundle
|
|
376
|
+
public_key_path: Path to public key (PEM format)
|
|
377
|
+
signature_path: Path to signature file (default: bundle.sig)
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
Tuple of (valid: bool, message: str)
|
|
381
|
+
"""
|
|
382
|
+
bundle_path = Path(bundle_path)
|
|
383
|
+
public_key_path = Path(public_key_path)
|
|
384
|
+
signature_path = Path(signature_path) if signature_path else bundle_path.with_suffix(".praison.sig")
|
|
385
|
+
|
|
386
|
+
if not bundle_path.exists():
|
|
387
|
+
return False, f"Bundle not found: {bundle_path}"
|
|
388
|
+
if not public_key_path.exists():
|
|
389
|
+
return False, f"Public key not found: {public_key_path}"
|
|
390
|
+
if not signature_path.exists():
|
|
391
|
+
return False, f"Signature not found: {signature_path}"
|
|
392
|
+
|
|
393
|
+
# Load signature
|
|
394
|
+
with open(signature_path) as f:
|
|
395
|
+
sig_data = json.load(f)
|
|
396
|
+
|
|
397
|
+
# Calculate current bundle hash
|
|
398
|
+
current_hash = _calculate_file_hash(bundle_path)
|
|
399
|
+
|
|
400
|
+
# Check hash matches
|
|
401
|
+
if current_hash != sig_data["hash"]:
|
|
402
|
+
return False, "Bundle hash mismatch - file may have been modified"
|
|
403
|
+
|
|
404
|
+
# Verify signature
|
|
405
|
+
try:
|
|
406
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
407
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
408
|
+
except ImportError:
|
|
409
|
+
return False, "cryptography package required for verification"
|
|
410
|
+
|
|
411
|
+
# Load public key
|
|
412
|
+
with open(public_key_path, "rb") as f:
|
|
413
|
+
public_key = serialization.load_pem_public_key(f.read())
|
|
414
|
+
|
|
415
|
+
# Verify
|
|
416
|
+
try:
|
|
417
|
+
signature = bytes.fromhex(sig_data["signature"])
|
|
418
|
+
public_key.verify(
|
|
419
|
+
signature,
|
|
420
|
+
sig_data["hash"].encode(),
|
|
421
|
+
padding.PKCS1v15(),
|
|
422
|
+
hashes.SHA256(),
|
|
423
|
+
)
|
|
424
|
+
return True, "Signature valid"
|
|
425
|
+
except Exception as e:
|
|
426
|
+
return False, f"Signature verification failed: {e}"
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _calculate_file_hash(path: Path) -> str:
|
|
430
|
+
"""Calculate SHA256 hash of a file."""
|
|
431
|
+
sha256 = hashlib.sha256()
|
|
432
|
+
with open(path, "rb") as f:
|
|
433
|
+
for chunk in iter(lambda: f.read(8192), b""):
|
|
434
|
+
sha256.update(chunk)
|
|
435
|
+
return sha256.hexdigest()
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# ============================================================================
|
|
439
|
+
# Dependency Auditing
|
|
440
|
+
# ============================================================================
|
|
441
|
+
|
|
442
|
+
def audit_dependencies(
|
|
443
|
+
recipe_path: Union[str, Path],
|
|
444
|
+
check_vulnerabilities: bool = True,
|
|
445
|
+
) -> Dict[str, Any]:
|
|
446
|
+
"""
|
|
447
|
+
Audit recipe dependencies for security issues.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
recipe_path: Path to recipe directory
|
|
451
|
+
check_vulnerabilities: Check for known vulnerabilities
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
Audit report dictionary
|
|
455
|
+
"""
|
|
456
|
+
recipe_path = Path(recipe_path)
|
|
457
|
+
|
|
458
|
+
report = {
|
|
459
|
+
"recipe": recipe_path.name,
|
|
460
|
+
"audited_at": _get_timestamp(),
|
|
461
|
+
"lockfile": None,
|
|
462
|
+
"dependencies": [],
|
|
463
|
+
"vulnerabilities": [],
|
|
464
|
+
"warnings": [],
|
|
465
|
+
"passed": True,
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
# Check for lockfile
|
|
469
|
+
lockfile = _find_lockfile(recipe_path)
|
|
470
|
+
if lockfile:
|
|
471
|
+
report["lockfile"] = str(lockfile)
|
|
472
|
+
else:
|
|
473
|
+
report["warnings"].append("No lockfile found - dependencies may not be reproducible")
|
|
474
|
+
|
|
475
|
+
# Get dependencies
|
|
476
|
+
deps = _get_python_deps(recipe_path)
|
|
477
|
+
report["dependencies"] = deps
|
|
478
|
+
|
|
479
|
+
# Check for vulnerabilities using pip-audit if available
|
|
480
|
+
if check_vulnerabilities and deps:
|
|
481
|
+
vulns = _check_vulnerabilities(deps)
|
|
482
|
+
report["vulnerabilities"] = vulns
|
|
483
|
+
if vulns:
|
|
484
|
+
report["passed"] = False
|
|
485
|
+
|
|
486
|
+
# Check for outdated dependencies
|
|
487
|
+
outdated = _check_outdated(deps)
|
|
488
|
+
if outdated:
|
|
489
|
+
report["warnings"].extend([
|
|
490
|
+
f"Outdated: {d['name']} ({d['current']} -> {d['latest']})"
|
|
491
|
+
for d in outdated
|
|
492
|
+
])
|
|
493
|
+
|
|
494
|
+
return report
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _find_lockfile(recipe_path: Path) -> Optional[Path]:
|
|
498
|
+
"""Find lockfile in recipe directory."""
|
|
499
|
+
lock_dir = recipe_path / "lock"
|
|
500
|
+
|
|
501
|
+
candidates = [
|
|
502
|
+
lock_dir / "uv.lock",
|
|
503
|
+
lock_dir / "requirements.lock",
|
|
504
|
+
lock_dir / "poetry.lock",
|
|
505
|
+
recipe_path / "uv.lock",
|
|
506
|
+
recipe_path / "requirements.lock",
|
|
507
|
+
recipe_path / "poetry.lock",
|
|
508
|
+
]
|
|
509
|
+
|
|
510
|
+
for candidate in candidates:
|
|
511
|
+
if candidate.exists():
|
|
512
|
+
return candidate
|
|
513
|
+
|
|
514
|
+
return None
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _check_vulnerabilities(deps: List[Dict[str, str]]) -> List[Dict[str, Any]]:
|
|
518
|
+
"""Check dependencies for known vulnerabilities."""
|
|
519
|
+
vulns = []
|
|
520
|
+
|
|
521
|
+
# Try using pip-audit
|
|
522
|
+
try:
|
|
523
|
+
result = subprocess.run(
|
|
524
|
+
[sys.executable, "-m", "pip_audit", "--format", "json"],
|
|
525
|
+
capture_output=True,
|
|
526
|
+
text=True,
|
|
527
|
+
timeout=60,
|
|
528
|
+
)
|
|
529
|
+
if result.returncode == 0:
|
|
530
|
+
audit_data = json.loads(result.stdout)
|
|
531
|
+
for vuln in audit_data.get("vulnerabilities", []):
|
|
532
|
+
vulns.append({
|
|
533
|
+
"package": vuln.get("name"),
|
|
534
|
+
"version": vuln.get("version"),
|
|
535
|
+
"vulnerability_id": vuln.get("id"),
|
|
536
|
+
"description": vuln.get("description"),
|
|
537
|
+
"fix_versions": vuln.get("fix_versions", []),
|
|
538
|
+
})
|
|
539
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError):
|
|
540
|
+
pass
|
|
541
|
+
|
|
542
|
+
return vulns
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _check_outdated(deps: List[Dict[str, str]]) -> List[Dict[str, str]]:
|
|
546
|
+
"""Check for outdated dependencies."""
|
|
547
|
+
# Simplified - would need PyPI API for full implementation
|
|
548
|
+
return []
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
# ============================================================================
|
|
552
|
+
# Lockfile Validation
|
|
553
|
+
# ============================================================================
|
|
554
|
+
|
|
555
|
+
def validate_lockfile(
|
|
556
|
+
recipe_path: Union[str, Path],
|
|
557
|
+
strict: bool = False,
|
|
558
|
+
) -> Dict[str, Any]:
|
|
559
|
+
"""
|
|
560
|
+
Validate recipe lockfile.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
recipe_path: Path to recipe directory
|
|
564
|
+
strict: Fail if lockfile missing
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
Validation result dictionary
|
|
568
|
+
"""
|
|
569
|
+
recipe_path = Path(recipe_path)
|
|
570
|
+
|
|
571
|
+
result = {
|
|
572
|
+
"valid": True,
|
|
573
|
+
"lockfile": None,
|
|
574
|
+
"lockfile_type": None,
|
|
575
|
+
"errors": [],
|
|
576
|
+
"warnings": [],
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
lockfile = _find_lockfile(recipe_path)
|
|
580
|
+
|
|
581
|
+
if not lockfile:
|
|
582
|
+
if strict:
|
|
583
|
+
result["valid"] = False
|
|
584
|
+
result["errors"].append("No lockfile found")
|
|
585
|
+
else:
|
|
586
|
+
result["warnings"].append("No lockfile found - dependencies may not be reproducible")
|
|
587
|
+
return result
|
|
588
|
+
|
|
589
|
+
result["lockfile"] = str(lockfile)
|
|
590
|
+
|
|
591
|
+
# Determine lockfile type
|
|
592
|
+
if "uv.lock" in lockfile.name:
|
|
593
|
+
result["lockfile_type"] = "uv"
|
|
594
|
+
elif "poetry.lock" in lockfile.name:
|
|
595
|
+
result["lockfile_type"] = "poetry"
|
|
596
|
+
else:
|
|
597
|
+
result["lockfile_type"] = "pip"
|
|
598
|
+
|
|
599
|
+
# Validate lockfile format
|
|
600
|
+
try:
|
|
601
|
+
if result["lockfile_type"] == "uv":
|
|
602
|
+
_parse_uv_lock(lockfile)
|
|
603
|
+
elif result["lockfile_type"] == "poetry":
|
|
604
|
+
_parse_poetry_lock(lockfile)
|
|
605
|
+
else:
|
|
606
|
+
_parse_requirements_lock(lockfile)
|
|
607
|
+
except Exception as e:
|
|
608
|
+
result["valid"] = False
|
|
609
|
+
result["errors"].append(f"Invalid lockfile format: {e}")
|
|
610
|
+
|
|
611
|
+
return result
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
# ============================================================================
|
|
615
|
+
# PII Redaction
|
|
616
|
+
# ============================================================================
|
|
617
|
+
|
|
618
|
+
# Default PII patterns
|
|
619
|
+
PII_PATTERNS = {
|
|
620
|
+
"email": r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
|
|
621
|
+
"phone": r"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b",
|
|
622
|
+
"ssn": r"\b\d{3}-\d{2}-\d{4}\b",
|
|
623
|
+
"credit_card": r"\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b",
|
|
624
|
+
"ip_address": r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b",
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def redact_pii(
|
|
629
|
+
data: Any,
|
|
630
|
+
policy: Optional[Dict[str, Any]] = None,
|
|
631
|
+
fields: Optional[List[str]] = None,
|
|
632
|
+
) -> Any:
|
|
633
|
+
"""
|
|
634
|
+
Redact PII from data based on policy.
|
|
635
|
+
|
|
636
|
+
Args:
|
|
637
|
+
data: Data to redact (dict, list, or string)
|
|
638
|
+
policy: Data policy configuration
|
|
639
|
+
fields: Specific fields to redact (overrides policy)
|
|
640
|
+
|
|
641
|
+
Returns:
|
|
642
|
+
Redacted data
|
|
643
|
+
"""
|
|
644
|
+
policy = policy or {}
|
|
645
|
+
mode = policy.get("pii", {}).get("mode", "allow")
|
|
646
|
+
|
|
647
|
+
if mode == "allow":
|
|
648
|
+
return data
|
|
649
|
+
|
|
650
|
+
fields = fields or policy.get("pii", {}).get("fields", list(PII_PATTERNS.keys()))
|
|
651
|
+
|
|
652
|
+
if isinstance(data, dict):
|
|
653
|
+
return {k: redact_pii(v, policy, fields) for k, v in data.items()}
|
|
654
|
+
elif isinstance(data, list):
|
|
655
|
+
return [redact_pii(item, policy, fields) for item in data]
|
|
656
|
+
elif isinstance(data, str):
|
|
657
|
+
return _redact_string(data, fields, mode)
|
|
658
|
+
else:
|
|
659
|
+
return data
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _redact_string(text: str, fields: List[str], mode: str) -> str:
|
|
663
|
+
"""Redact PII patterns from a string."""
|
|
664
|
+
for field in fields:
|
|
665
|
+
if field in PII_PATTERNS:
|
|
666
|
+
pattern = PII_PATTERNS[field]
|
|
667
|
+
if mode == "redact":
|
|
668
|
+
text = re.sub(pattern, f"[REDACTED:{field}]", text)
|
|
669
|
+
elif mode == "deny":
|
|
670
|
+
if re.search(pattern, text):
|
|
671
|
+
raise SecurityError(f"PII detected ({field}) and policy is 'deny'")
|
|
672
|
+
return text
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def detect_pii(
|
|
676
|
+
data: Any,
|
|
677
|
+
fields: Optional[List[str]] = None,
|
|
678
|
+
) -> List[Dict[str, Any]]:
|
|
679
|
+
"""
|
|
680
|
+
Detect PII in data without redacting.
|
|
681
|
+
|
|
682
|
+
Args:
|
|
683
|
+
data: Data to scan
|
|
684
|
+
fields: Specific fields to check
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
List of detected PII instances
|
|
688
|
+
"""
|
|
689
|
+
fields = fields or list(PII_PATTERNS.keys())
|
|
690
|
+
detections = []
|
|
691
|
+
|
|
692
|
+
def scan(value, path=""):
|
|
693
|
+
if isinstance(value, dict):
|
|
694
|
+
for k, v in value.items():
|
|
695
|
+
scan(v, f"{path}.{k}" if path else k)
|
|
696
|
+
elif isinstance(value, list):
|
|
697
|
+
for i, item in enumerate(value):
|
|
698
|
+
scan(item, f"{path}[{i}]")
|
|
699
|
+
elif isinstance(value, str):
|
|
700
|
+
for field in fields:
|
|
701
|
+
if field in PII_PATTERNS:
|
|
702
|
+
matches = re.findall(PII_PATTERNS[field], value)
|
|
703
|
+
for match in matches:
|
|
704
|
+
detections.append({
|
|
705
|
+
"type": field,
|
|
706
|
+
"path": path,
|
|
707
|
+
"sample": match[:4] + "..." if len(match) > 4 else match,
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
scan(data)
|
|
711
|
+
return detections
|