Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@v9.0.0
with:
python-version: ${{ matrix.python-version }}
- name: Install deps
Expand Down Expand Up @@ -141,7 +141,7 @@ jobs:
with:
fetch-depth: 0
- name: Set up uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@v9.0.0
with:
python-version: "3.10"
- name: Build dist
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@v9.0.0

- name: Sync dependencies (runtime + docs tooling)
run: uv sync --group docs --python 3.11
Expand Down
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ The optional visualization dependencies are available with:
pip install -e ".[visualization]"
```

Install the optional LangChain-compatible relationship tools with:

```bash
pip install -e ".[langchain]"
```

For AWS Bedrock agents, install the provider-specific extra:

```bash
pip install -e ".[bedrock]"
```

## Quick Start

```python
Expand Down Expand Up @@ -133,13 +145,14 @@ ip_obs = cv.observable_create(cv.OBS.IPV4, "192.0.2.1", internal=False)
cv.observable_add_relationship(
url_obs, # Can pass ObservableProxy directly
ip_obs, # Or use .key for string keys
cv.REL.RELATED_TO,
cv.DIR.BIDIRECTIONAL,
cv.REL.RESOLVES_TO,
)
```

Cyvest exposes enums for observable types and relationships via the facade (`cv.OBS`, `cv.REL`, `cv.DIR`)
so IDEs can autocomplete the official vocabulary without extra imports.
Built-ins cover `RELATED_TO`, `CONTAINS`, `DERIVED_FROM`, `RESOLVES_TO`,
`HOSTS`, `COMMUNICATES_WITH`, and `EXECUTES`.

Broad entity types use a subtype and, for locally scoped identifiers, a namespace:

Expand Down Expand Up @@ -493,7 +506,9 @@ Display differences in a rich table format:

```python
from cyvest.io_rich import display_diff
from logurich import logger
from logurich import get_logger

logger = get_logger(__name__)

# Display diff table with tree structure showing observables and threat intel
display_diff(diffs, lambda r: logger.rich("INFO", r), title="Investigation Diff")
Expand Down
115 changes: 115 additions & 0 deletions docs/agent-relationship-planning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Agent Relationship Planning

Cyvest exposes a framework-independent planning contract for agents that infer semantic relationships between existing observables. Agents propose semantics, while Cyvest owns graph integrity, scoring, audit, and mutation.

## Safe workflow

1. Read `get_relationship_catalog()` to understand the canonical vocabulary.
2. Read `cv.relationship_context_get()` to obtain observable keys, existing edges, and the graph revision.
3. Produce a structured `RelationshipPlan` using the returned revision.
4. Call `cv.relationship_plan_validate(plan)` without mutating the investigation.
5. Review errors and warnings, then call `cv.relationship_plan_apply(plan)` if approved.

Plans are rejected when their revision is stale. Application is atomic, recalculates scores once, and records the plan digest, model, tool, confidence, rationale, and evidence references in the audit log.

```python
from cyvest import Cyvest, RelationshipPlan, RelationshipProposal

cv = Cyvest()
domain = cv.observable(cv.OBS.DOMAIN, "example.com")
ip = cv.observable(cv.OBS.IPV4, "192.0.2.1")
context = cv.relationship_context_get()

plan = RelationshipPlan(
graph_revision=context.graph_revision,
model="relationship-planner-v1",
proposals=(
RelationshipProposal(
source_key=domain.key,
target_key=ip.key,
relationship_type="resolves-to",
confidence=0.96,
rationale="The address appeared in the domain's DNS answer.",
evidence_refs=("finding:dns-resolution",),
),
),
)

preview = cv.relationship_plan_validate(plan)
if preview.valid:
result = cv.relationship_plan_apply(plan, actor="dns-agent")
```

`RelationshipPlan.model_json_schema()` can be passed directly to providers that support structured output. The core planning API does not depend on an agent framework.

## LangChain and Deep Agents

Install the optional adapter:

```bash
pip install "cyvest[langchain]"
```

The generated tools implement LangChain's `BaseTool` contract and can be supplied to LangChain agents or Deep Agents.

```python
from cyvest.langchain import (
RELATIONSHIP_PLANNER_SYSTEM_PROMPT,
create_relationship_tools,
)

tools = create_relationship_tools(cv)
```

This toolkit is read-only by default. It contains tools for the relationship catalog, graph context, and plan validation.

Use the reusable prompt when creating a LangChain planner:

```python
from langchain.agents import create_agent

agent = create_agent(
model=model,
tools=tools,
system_prompt=RELATIONSHIP_PLANNER_SYSTEM_PROMPT,
response_format=RelationshipPlan,
)
```

The system prompt guides the planner through catalog, context, and validation tools. It instructs the agent to return the validated plan without applying it. After human or application approval, apply the plan outside the planner:

```python
result = cv.relationship_plan_apply(plan, actor="approved-relationship-agent")
```

For trusted orchestrators that require a tool interface, `create_relationship_tools(cv, include_apply=True)` adds the apply tool. Do not expose it to an autonomous planner as an approval boundary: an LLM can set `confirm=true` itself. Custom relationship types are rejected by default; set `allow_custom_types=True` only when the surrounding application governs that vocabulary.

The example requires a real model. For AWS Bedrock, use the project extra and a model available in your configured region:

```bash
uv run --extra bedrock python examples/07_langchain_relationships.py \
--model "bedrock_converse:anthropic.claude-3-5-sonnet-20240620-v1:0"
```

With pip, install `cyvest[bedrock]` before running the same script. `uv run --with langchain` is not equivalent: it installs LangChain itself, but not the `langchain-aws` provider integration.

LangChain uses the standard AWS credential chain and `AWS_REGION` or `AWS_DEFAULT_REGION`. The example raises an exception when `--model` is omitted. Its model-backed path uses `init_chat_model`, `RELATIONSHIP_PLANNER_SYSTEM_PROMPT`, the read-only Cyvest tools, and `response_format=RelationshipPlan`.

## Validation behavior

Errors prevent application in strict mode:

- stale graph revision
- unknown source or target keys
- self-relationships
- duplicate operations in one plan
- custom types when disabled
- removal of a relationship that does not exist
- a cycle in `contains` or `derived-from` relationships

Warnings preserve analyst flexibility:

- an add operation already exists
- a canonical relationship is unusual for the observable type pair
- a canonical relationship uses an unusual direction
- a high-confidence proposal uses the generic `related-to` type
4 changes: 3 additions & 1 deletion docs/comparing-investigations.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ Use `display_diff()` to render differences as a rich table:

```python
from cyvest.io_rich import display_diff
from logurich import logger
from logurich import get_logger

logger = get_logger(__name__)

display_diff(diffs, lambda r: logger.rich("INFO", r), title="Investigation Diff")
```
Expand Down
9 changes: 7 additions & 2 deletions docs/getting-started/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,7 @@ Relationships let you link observables together. Use the Cyvest facade or proxy
cv.observable_add_relationship(
source=url, # Observable proxy
target=ip, # Observable proxy
relationship_type=cv.REL.RELATED_TO,
relationship_type=cv.REL.RESOLVES_TO,
)

# Override direction to control score hierarchy
Expand All @@ -760,7 +760,12 @@ cv.observable_add_relationship(
)
```

`RELATED_TO` defaults to `BIDIRECTIONAL`. Choose `OUTBOUND` or `INBOUND` when you need explicit parent/child scoring.
Use the narrowest semantic type available: `CONTAINS` and `DERIVED_FROM` for
structure and lineage, `RESOLVES_TO` and `HOSTS` for infrastructure,
`COMMUNICATES_WITH` and `EXECUTES` for behavior, and `RELATED_TO` for
weak correlations. `RELATED_TO` and `COMMUNICATES_WITH` default to
`BIDIRECTIONAL`; the other built-ins default to `OUTBOUND`. An explicit
direction always wins and remains the source of score propagation semantics.

## Key Generation

Expand Down
2 changes: 1 addition & 1 deletion docs/js-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Interactive visualization of Cyvest observable relationships.

### Features

- **Observable Graph**: force-directed observable and relationship view centered on the root
- **Observable Explorer**: community-aware force graph centered on the root, with typed edges, search, filters, legend, and node/edge inspection
- **Restrained visual language**: neutral surfaces, compact SVG nodes, thin edges, and level color used only as a contour
- **Interactive focus**: pan/zoom, fit, deterministic layout replay, selection, and neighborhood focus on hover

Expand Down
5 changes: 2 additions & 3 deletions examples/01_email_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@
Demonstrates basic usage of Cyvest for analyzing a suspicious email.
"""

import logging
import tempfile
from decimal import Decimal
from pathlib import Path

from logurich import init_logger
from logurich import get_logger, init_logger

from cyvest import Cyvest

logger = logging.getLogger(__name__)
logger = get_logger(__name__)


def main() -> None:
Expand Down
5 changes: 2 additions & 3 deletions examples/02_urls_and_ips.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@
with relationship tracking.
"""

import logging
import tempfile
from decimal import Decimal
from pathlib import Path

from logurich import init_logger
from logurich import get_logger, init_logger

from cyvest import Cyvest

logger = logging.getLogger(__name__)
logger = get_logger(__name__)


def main() -> None:
Expand Down
5 changes: 2 additions & 3 deletions examples/03_merge_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@
and merge them together.
"""

import logging
import multiprocessing as mp
import tempfile
from decimal import Decimal
from pathlib import Path

from logurich import init_logger
from logurich import get_logger, init_logger

from cyvest import Cyvest

logger = logging.getLogger(__name__)
logger = get_logger(__name__)


def analyze_network_traffic() -> Cyvest:
Expand Down
Loading
Loading