attocode-code-intel 0.1.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.
- attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
- attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
- attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
- attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
- attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
- attocode_intel/GUIDELINES.md +432 -0
- attocode_intel/__init__.py +52 -0
- attocode_intel/_internal/__init__.py +1 -0
- attocode_intel/_internal/errors/__init__.py +126 -0
- attocode_intel/_internal/integrations/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
- attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
- attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
- attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
- attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
- attocode_intel/_internal/integrations/context/cas.py +466 -0
- attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
- attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
- attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
- attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
- attocode_intel/_internal/integrations/context/cross_references.py +616 -0
- attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
- attocode_intel/_internal/integrations/context/embeddings.py +282 -0
- attocode_intel/_internal/integrations/context/frecency.py +427 -0
- attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
- attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
- attocode_intel/_internal/integrations/context/hydration.py +90 -0
- attocode_intel/_internal/integrations/context/index_repair.py +87 -0
- attocode_intel/_internal/integrations/context/index_store.py +586 -0
- attocode_intel/_internal/integrations/context/memory_store.py +602 -0
- attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
- attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
- attocode_intel/_internal/integrations/context/query_history.py +417 -0
- attocode_intel/_internal/integrations/context/reranker.py +351 -0
- attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
- attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
- attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
- attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
- attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
- attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
- attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
- attocode_intel/_internal/integrations/context/vector_store.py +795 -0
- attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
- attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
- attocode_intel/_internal/integrations/lsp/client.py +1140 -0
- attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
- attocode_intel/_internal/integrations/mcp/client.py +253 -0
- attocode_intel/_internal/integrations/security/__init__.py +1 -0
- attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
- attocode_intel/_internal/integrations/security/dataflow.py +522 -0
- attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
- attocode_intel/_internal/integrations/security/matcher.py +44 -0
- attocode_intel/_internal/integrations/security/patterns.py +724 -0
- attocode_intel/_internal/integrations/security/scanner.py +351 -0
- attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
- attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
- attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
- attocode_intel/_internal/tricks/__init__.py +1 -0
- attocode_intel/_internal/tricks/recursive_context.py +357 -0
- attocode_intel/_internal/types/__init__.py +1 -0
- attocode_intel/_internal/types/events.py +289 -0
- attocode_intel/_internal/types/severity.py +15 -0
- attocode_intel/_shared.py +341 -0
- attocode_intel/analysis_coverage.py +56 -0
- attocode_intel/api/__init__.py +1 -0
- attocode_intel/api/access.py +32 -0
- attocode_intel/api/app.py +247 -0
- attocode_intel/api/auth/__init__.py +128 -0
- attocode_intel/api/auth/api_keys.py +61 -0
- attocode_intel/api/auth/context.py +34 -0
- attocode_intel/api/auth/jwt.py +66 -0
- attocode_intel/api/auth/oauth.py +146 -0
- attocode_intel/api/auth/passwords.py +15 -0
- attocode_intel/api/deps.py +351 -0
- attocode_intel/api/knowledge_bridge.py +13 -0
- attocode_intel/api/mcp_transport.py +51 -0
- attocode_intel/api/middleware.py +411 -0
- attocode_intel/api/models.py +698 -0
- attocode_intel/api/providers/__init__.py +1 -0
- attocode_intel/api/providers/db_provider.py +1106 -0
- attocode_intel/api/providers/local_provider.py +476 -0
- attocode_intel/api/providers/remote_provider.py +740 -0
- attocode_intel/api/repository_source.py +21 -0
- attocode_intel/api/routes/__init__.py +1 -0
- attocode_intel/api/routes/_pin_helper.py +158 -0
- attocode_intel/api/routes/activity.py +148 -0
- attocode_intel/api/routes/adr.py +102 -0
- attocode_intel/api/routes/analysis.py +515 -0
- attocode_intel/api/routes/api_keys.py +170 -0
- attocode_intel/api/routes/auth.py +428 -0
- attocode_intel/api/routes/branches.py +262 -0
- attocode_intel/api/routes/cross_repo_search.py +198 -0
- attocode_intel/api/routes/embeddings.py +386 -0
- attocode_intel/api/routes/files.py +132 -0
- attocode_intel/api/routes/files_v2.py +388 -0
- attocode_intel/api/routes/gc.py +322 -0
- attocode_intel/api/routes/git_v2.py +433 -0
- attocode_intel/api/routes/graph.py +174 -0
- attocode_intel/api/routes/graph_viz.py +160 -0
- attocode_intel/api/routes/health.py +62 -0
- attocode_intel/api/routes/history.py +175 -0
- attocode_intel/api/routes/jobs.py +84 -0
- attocode_intel/api/routes/learning.py +205 -0
- attocode_intel/api/routes/lsp.py +272 -0
- attocode_intel/api/routes/notify.py +623 -0
- attocode_intel/api/routes/operations.py +27 -0
- attocode_intel/api/routes/orgs.py +924 -0
- attocode_intel/api/routes/preferences.py +100 -0
- attocode_intel/api/routes/presence.py +42 -0
- attocode_intel/api/routes/projects.py +116 -0
- attocode_intel/api/routes/repos.py +47 -0
- attocode_intel/api/routes/search.py +180 -0
- attocode_intel/api/routes/snapshots.py +614 -0
- attocode_intel/api/routes/webhooks.py +313 -0
- attocode_intel/api/routes/websocket.py +116 -0
- attocode_intel/api/static/graph.html +239 -0
- attocode_intel/api/utils.py +96 -0
- attocode_intel/artifacts/__init__.py +45 -0
- attocode_intel/artifacts/hashing.py +230 -0
- attocode_intel/artifacts/provenance.py +190 -0
- attocode_intel/artifacts/retrieval_pin.py +159 -0
- attocode_intel/audit.py +59 -0
- attocode_intel/bug_finder.py +206 -0
- attocode_intel/bundle.py +80 -0
- attocode_intel/catalog.py +242 -0
- attocode_intel/change_review.py +75 -0
- attocode_intel/cli.py +3062 -0
- attocode_intel/community.py +63 -0
- attocode_intel/confidence/__init__.py +144 -0
- attocode_intel/confidence/jev.py +234 -0
- attocode_intel/confidence/llm.py +330 -0
- attocode_intel/confidence/redact.py +44 -0
- attocode_intel/confidence/settings.py +74 -0
- attocode_intel/config.py +256 -0
- attocode_intel/crypto.py +31 -0
- attocode_intel/db/__init__.py +7 -0
- attocode_intel/db/base.py +33 -0
- attocode_intel/db/engine.py +61 -0
- attocode_intel/db/models.py +529 -0
- attocode_intel/entrypoint.py +185 -0
- attocode_intel/focused_evidence.py +130 -0
- attocode_intel/freshness.py +154 -0
- attocode_intel/gateway.py +734 -0
- attocode_intel/git/__init__.py +7 -0
- attocode_intel/git/credentials.py +75 -0
- attocode_intel/git/manager.py +493 -0
- attocode_intel/git/models.py +96 -0
- attocode_intel/git/storage.py +98 -0
- attocode_intel/graph_query_parser.py +761 -0
- attocode_intel/helpers.py +712 -0
- attocode_intel/indexing/__init__.py +29 -0
- attocode_intel/indexing/debouncer.py +136 -0
- attocode_intel/indexing/delta_indexer.py +151 -0
- attocode_intel/indexing/full_indexer.py +323 -0
- attocode_intel/indexing/incremental.py +240 -0
- attocode_intel/indexing/parser.py +403 -0
- attocode_intel/indexing/summarizer.py +264 -0
- attocode_intel/installer.py +1896 -0
- attocode_intel/knowledge.py +269 -0
- attocode_intel/local_knowledge.py +77 -0
- attocode_intel/migrations/alembic.ini +36 -0
- attocode_intel/migrations/env.py +71 -0
- attocode_intel/migrations/script.py.mako +27 -0
- attocode_intel/migrations/versions/001_initial_schema.py +219 -0
- attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
- attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
- attocode_intel/migrations/versions/004_audit_events.py +38 -0
- attocode_intel/migrations/versions/005_user_preferences.py +30 -0
- attocode_intel/migrations/versions/006_google_oauth.py +27 -0
- attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
- attocode_intel/migrations/versions/008_symbol_references.py +37 -0
- attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
- attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
- attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
- attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
- attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
- attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
- attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
- attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
- attocode_intel/migrations/versions/017_provenance_table.py +102 -0
- attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
- attocode_intel/migrations/versions/019_symref_caller.py +42 -0
- attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
- attocode_intel/onboarding.py +258 -0
- attocode_intel/output.py +178 -0
- attocode_intel/precision.py +133 -0
- attocode_intel/presence.py +160 -0
- attocode_intel/probe.py +110 -0
- attocode_intel/project_dir.py +97 -0
- attocode_intel/pubsub.py +113 -0
- attocode_intel/query_ranking.py +242 -0
- attocode_intel/readiness.py +972 -0
- attocode_intel/redis.py +51 -0
- attocode_intel/reference_pages.py +60 -0
- attocode_intel/remote.py +131 -0
- attocode_intel/repo_ranker.py +246 -0
- attocode_intel/request_context.py +82 -0
- attocode_intel/rules/__init__.py +28 -0
- attocode_intel/rules/ci.py +320 -0
- attocode_intel/rules/combinators.py +293 -0
- attocode_intel/rules/enricher.py +127 -0
- attocode_intel/rules/evolution.py +452 -0
- attocode_intel/rules/executor.py +531 -0
- attocode_intel/rules/filters/__init__.py +5 -0
- attocode_intel/rules/filters/pipeline.py +151 -0
- attocode_intel/rules/formatter.py +198 -0
- attocode_intel/rules/hygiene.py +242 -0
- attocode_intel/rules/importers/__init__.py +1 -0
- attocode_intel/rules/importers/semgrep.py +382 -0
- attocode_intel/rules/loader.py +292 -0
- attocode_intel/rules/marketplace.py +375 -0
- attocode_intel/rules/metavar.py +181 -0
- attocode_intel/rules/model.py +317 -0
- attocode_intel/rules/packs/__init__.py +1 -0
- attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
- attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
- attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
- attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
- attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
- attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
- attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
- attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
- attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
- attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
- attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
- attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
- attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
- attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
- attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
- attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
- attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
- attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
- attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
- attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
- attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
- attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
- attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
- attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
- attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
- attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
- attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
- attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
- attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
- attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
- attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
- attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
- attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/pack_loader.py +244 -0
- attocode_intel/rules/plugins/__init__.py +1 -0
- attocode_intel/rules/plugins/plugin_loader.py +132 -0
- attocode_intel/rules/profiling.py +332 -0
- attocode_intel/rules/registry.py +213 -0
- attocode_intel/rules/sarif.py +187 -0
- attocode_intel/rules/synthesis.py +420 -0
- attocode_intel/rules/testing.py +326 -0
- attocode_intel/server.py +734 -0
- attocode_intel/service.py +3416 -0
- attocode_intel/sharing.py +57 -0
- attocode_intel/storage/__init__.py +6 -0
- attocode_intel/storage/branch_overlay.py +352 -0
- attocode_intel/storage/content_store.py +220 -0
- attocode_intel/storage/dependency_store.py +143 -0
- attocode_intel/storage/diff_engine.py +187 -0
- attocode_intel/storage/embedding_store.py +594 -0
- attocode_intel/storage/provenance_store.py +137 -0
- attocode_intel/storage/security_scanner_db.py +164 -0
- attocode_intel/storage/symbol_store.py +160 -0
- attocode_intel/structural_evidence.py +144 -0
- attocode_intel/symbol_inspection.py +141 -0
- attocode_intel/symbol_links.py +167 -0
- attocode_intel/telemetry.py +42 -0
- attocode_intel/test_ranking.py +73 -0
- attocode_intel/testing/__init__.py +43 -0
- attocode_intel/testing/fixtures.py +122 -0
- attocode_intel/testing/helpers.py +75 -0
- attocode_intel/testing/mocks.py +99 -0
- attocode_intel/tools/__init__.py +2 -0
- attocode_intel/tools/adr_tools.py +628 -0
- attocode_intel/tools/analysis_tools.py +382 -0
- attocode_intel/tools/composite_tools.py +695 -0
- attocode_intel/tools/cross_mode_tools.py +173 -0
- attocode_intel/tools/dead_code_tools.py +582 -0
- attocode_intel/tools/distill_tools.py +353 -0
- attocode_intel/tools/frecency_tools.py +220 -0
- attocode_intel/tools/fuzzy_tools.py +255 -0
- attocode_intel/tools/history_tools.py +486 -0
- attocode_intel/tools/learning_tools.py +222 -0
- attocode_intel/tools/lsp_tools.py +218 -0
- attocode_intel/tools/maintenance_tools.py +1662 -0
- attocode_intel/tools/navigation_tools.py +220 -0
- attocode_intel/tools/overlay_tools.py +540 -0
- attocode_intel/tools/pin_store.py +432 -0
- attocode_intel/tools/pin_tools.py +266 -0
- attocode_intel/tools/query_constraints_tools.py +262 -0
- attocode_intel/tools/query_history_tools.py +158 -0
- attocode_intel/tools/readiness_tools.py +44 -0
- attocode_intel/tools/rule_tools.py +992 -0
- attocode_intel/tools/search_tools.py +770 -0
- attocode_intel/tools/snapshot_tools.py +624 -0
- attocode_intel/workers/__init__.py +1 -0
- attocode_intel/workers/job_utils.py +111 -0
- attocode_intel/workers/jobs.py +573 -0
- attocode_intel/workers/run.py +30 -0
- attocode_intel/workers/settings.py +61 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Internal code intelligence support; not a public API."""
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""MCP (Model Context Protocol) client.
|
|
2
|
+
|
|
3
|
+
Implements a stdio-based JSON-RPC client for communicating
|
|
4
|
+
with MCP servers that provide additional tools.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _expand_env(env: dict[str, str] | None) -> dict[str, str] | None:
|
|
21
|
+
"""Expand ``${VAR}`` references and merge with the current environment.
|
|
22
|
+
|
|
23
|
+
When *env* is provided, we start from ``os.environ`` (so the child
|
|
24
|
+
process inherits PATH, HOME, etc.) and overlay the user-supplied
|
|
25
|
+
values after expanding any ``${VAR_NAME}`` patterns against
|
|
26
|
+
``os.environ``.
|
|
27
|
+
"""
|
|
28
|
+
if not env:
|
|
29
|
+
return None
|
|
30
|
+
base = os.environ.copy()
|
|
31
|
+
for key, val in env.items():
|
|
32
|
+
expanded = re.sub(
|
|
33
|
+
r"\$\{(?:env:)?(\w+)\}",
|
|
34
|
+
lambda m: os.environ.get(m.group(1), ""),
|
|
35
|
+
val,
|
|
36
|
+
)
|
|
37
|
+
base[key] = expanded
|
|
38
|
+
return base
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class MCPTool:
|
|
43
|
+
"""A tool provided by an MCP server."""
|
|
44
|
+
|
|
45
|
+
name: str
|
|
46
|
+
description: str
|
|
47
|
+
input_schema: dict[str, Any] = field(default_factory=dict)
|
|
48
|
+
server_name: str = ""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class MCPCallResult:
|
|
53
|
+
"""Result of calling an MCP tool."""
|
|
54
|
+
|
|
55
|
+
success: bool
|
|
56
|
+
result: Any = None
|
|
57
|
+
error: str | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class MCPClient:
|
|
61
|
+
"""Stdio-based JSON-RPC MCP client.
|
|
62
|
+
|
|
63
|
+
Launches an MCP server process, communicates via stdin/stdout
|
|
64
|
+
using JSON-RPC 2.0 protocol.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
server_command: str,
|
|
70
|
+
server_args: list[str] | None = None,
|
|
71
|
+
server_name: str = "",
|
|
72
|
+
env: dict[str, str] | None = None,
|
|
73
|
+
cwd: str | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
self._command = server_command
|
|
76
|
+
self._args = server_args or []
|
|
77
|
+
self._server_name = server_name
|
|
78
|
+
self._env = env
|
|
79
|
+
self._cwd = cwd
|
|
80
|
+
self._process: asyncio.subprocess.Process | None = None
|
|
81
|
+
self._tools: list[MCPTool] = []
|
|
82
|
+
self._initialized = False
|
|
83
|
+
self._request_id = 0
|
|
84
|
+
|
|
85
|
+
async def connect(self) -> None:
|
|
86
|
+
"""Start the MCP server process and initialize."""
|
|
87
|
+
self._process = await asyncio.create_subprocess_exec(
|
|
88
|
+
self._command,
|
|
89
|
+
*self._args,
|
|
90
|
+
stdin=asyncio.subprocess.PIPE,
|
|
91
|
+
stdout=asyncio.subprocess.PIPE,
|
|
92
|
+
stderr=asyncio.subprocess.PIPE,
|
|
93
|
+
env=_expand_env(self._env),
|
|
94
|
+
cwd=self._cwd,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Send initialize request
|
|
98
|
+
result = await self._request("initialize", {
|
|
99
|
+
"protocolVersion": "2024-11-05",
|
|
100
|
+
"capabilities": {},
|
|
101
|
+
"clientInfo": {"name": "attocode", "version": "0.1.0"},
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
if result is not None:
|
|
105
|
+
self._initialized = True
|
|
106
|
+
|
|
107
|
+
# Send initialized notification
|
|
108
|
+
await self._notify("notifications/initialized", {})
|
|
109
|
+
|
|
110
|
+
# List tools
|
|
111
|
+
await self._refresh_tools()
|
|
112
|
+
|
|
113
|
+
async def disconnect(self) -> None:
|
|
114
|
+
"""Disconnect and terminate the server process."""
|
|
115
|
+
if self._process:
|
|
116
|
+
try:
|
|
117
|
+
if self._process.stdin:
|
|
118
|
+
self._process.stdin.close()
|
|
119
|
+
self._process.terminate()
|
|
120
|
+
await asyncio.wait_for(self._process.wait(), timeout=5.0)
|
|
121
|
+
except (TimeoutError, ProcessLookupError):
|
|
122
|
+
self._process.kill()
|
|
123
|
+
finally:
|
|
124
|
+
self._process = None
|
|
125
|
+
self._initialized = False
|
|
126
|
+
|
|
127
|
+
async def call_tool(self, name: str, arguments: dict[str, Any]) -> MCPCallResult:
|
|
128
|
+
"""Call an MCP tool."""
|
|
129
|
+
if not self._initialized:
|
|
130
|
+
return MCPCallResult(success=False, error="Not connected")
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
result = await self._request("tools/call", {
|
|
134
|
+
"name": name,
|
|
135
|
+
"arguments": arguments,
|
|
136
|
+
})
|
|
137
|
+
if result is None:
|
|
138
|
+
return MCPCallResult(success=False, error="No response")
|
|
139
|
+
|
|
140
|
+
# MCP tool results have a 'content' array
|
|
141
|
+
content = result.get("content", [])
|
|
142
|
+
if content and isinstance(content, list):
|
|
143
|
+
text_parts = [
|
|
144
|
+
c.get("text", "")
|
|
145
|
+
for c in content
|
|
146
|
+
if c.get("type") == "text"
|
|
147
|
+
]
|
|
148
|
+
return MCPCallResult(success=True, result="\n".join(text_parts))
|
|
149
|
+
|
|
150
|
+
return MCPCallResult(success=True, result=str(result))
|
|
151
|
+
except Exception as e:
|
|
152
|
+
return MCPCallResult(success=False, error=str(e))
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def tools(self) -> list[MCPTool]:
|
|
156
|
+
"""Get available tools."""
|
|
157
|
+
return list(self._tools)
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def is_connected(self) -> bool:
|
|
161
|
+
return self._initialized
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def server_name(self) -> str:
|
|
165
|
+
return self._server_name
|
|
166
|
+
|
|
167
|
+
async def _refresh_tools(self) -> None:
|
|
168
|
+
"""Refresh the tool list from the server."""
|
|
169
|
+
result = await self._request("tools/list", {})
|
|
170
|
+
if result and "tools" in result:
|
|
171
|
+
self._tools = [
|
|
172
|
+
MCPTool(
|
|
173
|
+
name=t["name"],
|
|
174
|
+
description=t.get("description", ""),
|
|
175
|
+
input_schema=t.get("inputSchema", {}),
|
|
176
|
+
server_name=self._server_name,
|
|
177
|
+
)
|
|
178
|
+
for t in result["tools"]
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
async def _request(self, method: str, params: dict[str, Any]) -> Any:
|
|
182
|
+
"""Send a JSON-RPC request and wait for response."""
|
|
183
|
+
if not self._process or not self._process.stdin or not self._process.stdout:
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
self._request_id += 1
|
|
187
|
+
req_id = self._request_id
|
|
188
|
+
msg = {
|
|
189
|
+
"jsonrpc": "2.0",
|
|
190
|
+
"id": req_id,
|
|
191
|
+
"method": method,
|
|
192
|
+
"params": params,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
data = json.dumps(msg) + "\n"
|
|
196
|
+
self._process.stdin.write(data.encode())
|
|
197
|
+
await self._process.stdin.drain()
|
|
198
|
+
|
|
199
|
+
# Read response
|
|
200
|
+
try:
|
|
201
|
+
line = await asyncio.wait_for(
|
|
202
|
+
self._process.stdout.readline(), timeout=30.0
|
|
203
|
+
)
|
|
204
|
+
if not line:
|
|
205
|
+
logger.warning(
|
|
206
|
+
"MCP server %s returned empty response for %s",
|
|
207
|
+
self._server_name, method,
|
|
208
|
+
)
|
|
209
|
+
return None
|
|
210
|
+
response = json.loads(line.decode())
|
|
211
|
+
# Validate JSON-RPC response ID matches request
|
|
212
|
+
resp_id = response.get("id")
|
|
213
|
+
if resp_id != req_id:
|
|
214
|
+
logger.warning(
|
|
215
|
+
"MCP server %s response ID mismatch for %s: expected %s, got %s",
|
|
216
|
+
self._server_name, method, req_id, resp_id,
|
|
217
|
+
)
|
|
218
|
+
if "error" in response:
|
|
219
|
+
err = response["error"]
|
|
220
|
+
logger.warning(
|
|
221
|
+
"MCP server %s RPC error for %s: code=%s message=%s",
|
|
222
|
+
self._server_name, method,
|
|
223
|
+
err.get("code", "?"), err.get("message", "unknown"),
|
|
224
|
+
)
|
|
225
|
+
return None
|
|
226
|
+
return response.get("result")
|
|
227
|
+
except TimeoutError:
|
|
228
|
+
logger.warning(
|
|
229
|
+
"MCP server %s timed out on %s (30s)",
|
|
230
|
+
self._server_name, method,
|
|
231
|
+
)
|
|
232
|
+
return None
|
|
233
|
+
except json.JSONDecodeError as exc:
|
|
234
|
+
logger.warning(
|
|
235
|
+
"MCP server %s returned invalid JSON for %s: %s",
|
|
236
|
+
self._server_name, method, exc,
|
|
237
|
+
)
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
async def _notify(self, method: str, params: dict[str, Any]) -> None:
|
|
241
|
+
"""Send a JSON-RPC notification (no response expected)."""
|
|
242
|
+
if not self._process or not self._process.stdin:
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
msg = {
|
|
246
|
+
"jsonrpc": "2.0",
|
|
247
|
+
"method": method,
|
|
248
|
+
"params": params,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
data = json.dumps(msg) + "\n"
|
|
252
|
+
self._process.stdin.write(data.encode())
|
|
253
|
+
await self._process.stdin.drain()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Internal code intelligence support; not a public API."""
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Load user-defined security rules from .attocode/rules/*.yaml files.
|
|
2
|
+
|
|
3
|
+
Custom rules use the same SecurityPattern dataclass as built-in patterns,
|
|
4
|
+
allowing them to be seamlessly merged into the scanning pipeline.
|
|
5
|
+
|
|
6
|
+
Rule format (YAML):
|
|
7
|
+
id: my_rule_name
|
|
8
|
+
pattern: "regex_pattern_here"
|
|
9
|
+
message: "What was detected"
|
|
10
|
+
severity: high # critical | high | medium | low | info
|
|
11
|
+
cwe: CWE-79 # optional
|
|
12
|
+
recommendation: "How to fix"
|
|
13
|
+
languages: # optional, empty = all languages
|
|
14
|
+
- python
|
|
15
|
+
- javascript
|
|
16
|
+
scan_comments: false # optional, default false
|
|
17
|
+
fix: # optional autofix
|
|
18
|
+
search: "old_code("
|
|
19
|
+
replace: "new_code("
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
import re
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from attocode_intel._internal.integrations.security.patterns import (
|
|
29
|
+
Category,
|
|
30
|
+
SecurityPattern,
|
|
31
|
+
Severity,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
_VALID_SEVERITIES = frozenset(s.value for s in Severity)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_custom_rules(project_dir: str) -> list[SecurityPattern]:
|
|
40
|
+
"""Load custom security rules from .attocode/rules/*.yaml files.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
project_dir: Project root directory.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
List of SecurityPattern instances from user-defined rules.
|
|
47
|
+
Invalid rules are logged and skipped.
|
|
48
|
+
"""
|
|
49
|
+
rules_dir = Path(project_dir) / ".attocode" / "rules"
|
|
50
|
+
if not rules_dir.is_dir():
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
patterns: list[SecurityPattern] = []
|
|
54
|
+
for yaml_file in sorted(rules_dir.glob("*.yaml")):
|
|
55
|
+
try:
|
|
56
|
+
file_patterns = _load_rules_file(yaml_file)
|
|
57
|
+
patterns.extend(file_patterns)
|
|
58
|
+
except Exception as exc:
|
|
59
|
+
logger.warning("Failed to load custom rules from %s: %s", yaml_file, exc)
|
|
60
|
+
|
|
61
|
+
if patterns:
|
|
62
|
+
logger.info("Loaded %d custom security rule(s) from %s", len(patterns), rules_dir)
|
|
63
|
+
|
|
64
|
+
return patterns
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _load_rules_file(path: Path) -> list[SecurityPattern]:
|
|
68
|
+
"""Parse a single YAML rules file into SecurityPattern instances."""
|
|
69
|
+
try:
|
|
70
|
+
import yaml # type: ignore[import-untyped]
|
|
71
|
+
except ImportError:
|
|
72
|
+
logger.debug("PyYAML not installed — skipping custom rules file %s", path)
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
content = path.read_text(encoding="utf-8")
|
|
76
|
+
data = yaml.safe_load(content)
|
|
77
|
+
|
|
78
|
+
if data is None:
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
# Support both single-rule and multi-rule files
|
|
82
|
+
if isinstance(data, dict):
|
|
83
|
+
rules_list = [data]
|
|
84
|
+
elif isinstance(data, list):
|
|
85
|
+
rules_list = data
|
|
86
|
+
else:
|
|
87
|
+
logger.warning("Invalid rules file %s: expected dict or list", path)
|
|
88
|
+
return []
|
|
89
|
+
|
|
90
|
+
patterns: list[SecurityPattern] = []
|
|
91
|
+
for i, rule in enumerate(rules_list):
|
|
92
|
+
try:
|
|
93
|
+
pattern = _parse_rule(rule, source=f"{path.name}[{i}]")
|
|
94
|
+
if pattern is not None:
|
|
95
|
+
patterns.append(pattern)
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
logger.warning("Invalid rule in %s at index %d: %s", path.name, i, exc)
|
|
98
|
+
|
|
99
|
+
return patterns
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _parse_rule(rule: dict, source: str = "") -> SecurityPattern | None:
|
|
103
|
+
"""Parse a single rule dict into a SecurityPattern.
|
|
104
|
+
|
|
105
|
+
Required fields: id, pattern, message, severity, recommendation.
|
|
106
|
+
Optional fields: cwe, languages, scan_comments, fix.
|
|
107
|
+
"""
|
|
108
|
+
# Validate required fields
|
|
109
|
+
missing = [f for f in ("id", "pattern", "message", "severity", "recommendation") if f not in rule]
|
|
110
|
+
if missing:
|
|
111
|
+
logger.warning("Rule %s missing required fields: %s", source, ", ".join(missing))
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
# Validate severity
|
|
115
|
+
severity_str = str(rule["severity"]).lower()
|
|
116
|
+
if severity_str not in _VALID_SEVERITIES:
|
|
117
|
+
logger.warning(
|
|
118
|
+
"Rule %s has invalid severity '%s' (must be one of: %s)",
|
|
119
|
+
source, rule["severity"], ", ".join(sorted(_VALID_SEVERITIES)),
|
|
120
|
+
)
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
# Compile regex
|
|
124
|
+
try:
|
|
125
|
+
compiled = re.compile(rule["pattern"])
|
|
126
|
+
except re.error as exc:
|
|
127
|
+
logger.warning("Rule %s has invalid regex '%s': %s", source, rule["pattern"], exc)
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
return SecurityPattern(
|
|
131
|
+
name=str(rule["id"]),
|
|
132
|
+
pattern=compiled,
|
|
133
|
+
severity=Severity(severity_str),
|
|
134
|
+
category=Category.ANTI_PATTERN,
|
|
135
|
+
cwe_id=str(rule.get("cwe", "")),
|
|
136
|
+
message=str(rule["message"]),
|
|
137
|
+
recommendation=str(rule["recommendation"]),
|
|
138
|
+
languages=rule.get("languages", []),
|
|
139
|
+
scan_comments=bool(rule.get("scan_comments", False)),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_autofix_from_rules(
|
|
144
|
+
project_dir: str,
|
|
145
|
+
) -> dict[str, tuple[str, str]]:
|
|
146
|
+
"""Extract autofix templates from custom rules that define a 'fix' field.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Dict mapping rule id -> (search, replace) for rules with fix defined.
|
|
150
|
+
"""
|
|
151
|
+
rules_dir = Path(project_dir) / ".attocode" / "rules"
|
|
152
|
+
if not rules_dir.is_dir():
|
|
153
|
+
return {}
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
import yaml # type: ignore[import-untyped]
|
|
157
|
+
except ImportError:
|
|
158
|
+
return {}
|
|
159
|
+
|
|
160
|
+
fixes: dict[str, tuple[str, str]] = {}
|
|
161
|
+
for yaml_file in sorted(rules_dir.glob("*.yaml")):
|
|
162
|
+
try:
|
|
163
|
+
content = yaml_file.read_text(encoding="utf-8")
|
|
164
|
+
data = yaml.safe_load(content)
|
|
165
|
+
if data is None:
|
|
166
|
+
continue
|
|
167
|
+
rules_list = [data] if isinstance(data, dict) else data if isinstance(data, list) else []
|
|
168
|
+
for rule in rules_list:
|
|
169
|
+
if isinstance(rule, dict) and "fix" in rule and "id" in rule:
|
|
170
|
+
fix = rule["fix"]
|
|
171
|
+
if isinstance(fix, dict) and "search" in fix and "replace" in fix:
|
|
172
|
+
fixes[str(rule["id"])] = (str(fix["search"]), str(fix["replace"]))
|
|
173
|
+
except Exception:
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
return fixes
|