mimvo 0.2.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.
- mimvo/__init__.py +53 -0
- mimvo/_coerce.py +72 -0
- mimvo/_version.py +11 -0
- mimvo/checks/__init__.py +54 -0
- mimvo/checks/base.py +149 -0
- mimvo/checks/security/__init__.py +43 -0
- mimvo/checks/security/_hf.py +439 -0
- mimvo/checks/security/_http.py +62 -0
- mimvo/checks/security/_phrases.py +180 -0
- mimvo/checks/security/base.py +780 -0
- mimvo/checks/security/injection/__init__.py +48 -0
- mimvo/checks/security/injection/check.py +45 -0
- mimvo/checks/security/injection/datasentinel.py +81 -0
- mimvo/checks/security/injection/deepset_deberta.py +31 -0
- mimvo/checks/security/injection/embedding_clf.py +76 -0
- mimvo/checks/security/injection/heuristic.py +429 -0
- mimvo/checks/security/injection/known_answer.py +76 -0
- mimvo/checks/security/injection/lakera_guard.py +129 -0
- mimvo/checks/security/injection/probes.py +80 -0
- mimvo/checks/security/injection/prompt_guard.py +37 -0
- mimvo/checks/security/injection/prompt_shield.py +127 -0
- mimvo/checks/security/injection/protectai_deberta.py +31 -0
- mimvo/checks/security/injection/sentinel.py +44 -0
- mimvo/checks/security/poisoning/__init__.py +50 -0
- mimvo/checks/security/poisoning/_corpus.py +28 -0
- mimvo/checks/security/poisoning/check.py +48 -0
- mimvo/checks/security/poisoning/consistency.py +101 -0
- mimvo/checks/security/poisoning/heuristic.py +453 -0
- mimvo/checks/security/poisoning/hubness.py +109 -0
- mimvo/checks/security/poisoning/nli.py +127 -0
- mimvo/checks/security/poisoning/perplexity.py +140 -0
- mimvo/checks/security/poisoning/probe.py +115 -0
- mimvo/checks/security/poisoning/raguard.py +149 -0
- mimvo/checks/security/poisoning/revprag.py +51 -0
- mimvo/checks/security/poisoning/trustrag.py +192 -0
- mimvo/checks/security/secrets/__init__.py +86 -0
- mimvo/checks/security/secrets/check.py +51 -0
- mimvo/checks/security/secrets/detect_secrets.py +118 -0
- mimvo/checks/security/secrets/entropy.py +156 -0
- mimvo/checks/security/secrets/gitleaks.py +79 -0
- mimvo/checks/security/secrets/gliner2_pii.py +184 -0
- mimvo/checks/security/secrets/gliner_pii.py +150 -0
- mimvo/checks/security/secrets/heuristic.py +151 -0
- mimvo/checks/security/secrets/piiranha.py +77 -0
- mimvo/checks/security/secrets/presidio.py +161 -0
- mimvo/checks/security/secrets/starpii.py +54 -0
- mimvo/checks/security/secrets/verify.py +62 -0
- mimvo/cli.py +556 -0
- mimvo/client.py +205 -0
- mimvo/config.py +45 -0
- mimvo/context.py +107 -0
- mimvo/corpus.py +708 -0
- mimvo/engine.py +350 -0
- mimvo/exceptions.py +56 -0
- mimvo/integrations/__init__.py +16 -0
- mimvo/integrations/hook.py +216 -0
- mimvo/mask.py +87 -0
- mimvo/models/__init__.py +36 -0
- mimvo/models/enums.py +171 -0
- mimvo/models/finding.py +76 -0
- mimvo/models/memory.py +79 -0
- mimvo/models/results.py +366 -0
- mimvo/owasp.py +39 -0
- mimvo/plugins.py +142 -0
- mimvo/py.typed +0 -0
- mimvo/reporters/__init__.py +17 -0
- mimvo/reporters/html.py +900 -0
- mimvo/reporters/markdown.py +267 -0
- mimvo/reporters/sarif.py +222 -0
- mimvo/reporters/terminal.py +177 -0
- mimvo/rules.py +486 -0
- mimvo/sources/__init__.py +33 -0
- mimvo/sources/base.py +261 -0
- mimvo/sources/chroma.py +141 -0
- mimvo/sources/jsonl.py +85 -0
- mimvo/sources/langchain.py +251 -0
- mimvo/sources/langgraph.py +137 -0
- mimvo/sources/mem0.py +238 -0
- mimvo/sources/pgvector.py +160 -0
- mimvo/sources/pinecone.py +154 -0
- mimvo/sources/qdrant.py +140 -0
- mimvo/telemetry/__init__.py +116 -0
- mimvo/telemetry/otel.py +79 -0
- mimvo/text.py +469 -0
- mimvo/vectors.py +155 -0
- mimvo-0.2.0.dist-info/METADATA +684 -0
- mimvo-0.2.0.dist-info/RECORD +90 -0
- mimvo-0.2.0.dist-info/WHEEL +4 -0
- mimvo-0.2.0.dist-info/entry_points.txt +2 -0
- mimvo-0.2.0.dist-info/licenses/LICENSE +201 -0
mimvo/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Scan agent memory for poisoned facts, hidden instructions, and leaked secrets.
|
|
2
|
+
|
|
3
|
+
From the command line:
|
|
4
|
+
|
|
5
|
+
mimvo scan chroma --path ./chroma_db --collection agent_memory
|
|
6
|
+
mimvo scan jsonl export.jsonl --report report.html --json findings.json
|
|
7
|
+
|
|
8
|
+
From Python, create a `Mimvo` and call `scan`. The scan reads records and
|
|
9
|
+
returns a `ScanReport`. It does not change the store.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from ._version import __version__
|
|
15
|
+
from .client import AsyncMimvo, Mimvo
|
|
16
|
+
from .config import Config
|
|
17
|
+
from .exceptions import (
|
|
18
|
+
BackendError,
|
|
19
|
+
ConfigurationError,
|
|
20
|
+
MimvoError,
|
|
21
|
+
)
|
|
22
|
+
from .models import (
|
|
23
|
+
Action,
|
|
24
|
+
Category,
|
|
25
|
+
Finding,
|
|
26
|
+
MemoryCandidate,
|
|
27
|
+
MemoryRecord,
|
|
28
|
+
MemoryStatus,
|
|
29
|
+
Risk,
|
|
30
|
+
ScanError,
|
|
31
|
+
ScanReport,
|
|
32
|
+
Severity,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"Action",
|
|
37
|
+
"AsyncMimvo",
|
|
38
|
+
"BackendError",
|
|
39
|
+
"Category",
|
|
40
|
+
"Config",
|
|
41
|
+
"ConfigurationError",
|
|
42
|
+
"Finding",
|
|
43
|
+
"MemoryCandidate",
|
|
44
|
+
"MemoryRecord",
|
|
45
|
+
"MemoryStatus",
|
|
46
|
+
"Mimvo",
|
|
47
|
+
"MimvoError",
|
|
48
|
+
"Risk",
|
|
49
|
+
"ScanError",
|
|
50
|
+
"ScanReport",
|
|
51
|
+
"Severity",
|
|
52
|
+
"__version__",
|
|
53
|
+
]
|
mimvo/_coerce.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Turn loose caller input into `MemoryCandidate` and `MemoryRecord`.
|
|
2
|
+
|
|
3
|
+
Callers can pass a string, a dict, or an existing model. These helpers
|
|
4
|
+
accept all three so the common case stays short.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .exceptions import ConfigurationError
|
|
10
|
+
from .models.memory import MemoryCandidate, MemoryRecord
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def coerce_candidate(content: str | MemoryCandidate | dict) -> MemoryCandidate:
|
|
14
|
+
"""Turn one input into a `MemoryCandidate`.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
content: The memory to scan. A string becomes the `content` field.
|
|
18
|
+
A dict is validated as a `MemoryCandidate`. A `MemoryCandidate`
|
|
19
|
+
is returned unchanged.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
A `MemoryCandidate` the checks can read.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
ConfigurationError: `content` is not a string, a dict, or a
|
|
26
|
+
`MemoryCandidate`.
|
|
27
|
+
"""
|
|
28
|
+
if isinstance(content, MemoryCandidate):
|
|
29
|
+
return content
|
|
30
|
+
if isinstance(content, dict):
|
|
31
|
+
return MemoryCandidate.model_validate(content)
|
|
32
|
+
if isinstance(content, str):
|
|
33
|
+
return MemoryCandidate(content=content)
|
|
34
|
+
raise ConfigurationError(f"Cannot interpret content: {content!r}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def coerce_record(item: object) -> MemoryRecord:
|
|
38
|
+
"""Turn one input into a `MemoryRecord`.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
item: A `MemoryRecord`, returned unchanged, or a dict with at least
|
|
42
|
+
an `id` and a `content` string.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
A `MemoryRecord` the scan engine can read.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
ConfigurationError: `item` is not a record or a dict.
|
|
49
|
+
"""
|
|
50
|
+
if isinstance(item, MemoryRecord):
|
|
51
|
+
return item
|
|
52
|
+
if isinstance(item, dict):
|
|
53
|
+
return MemoryRecord.model_validate(item)
|
|
54
|
+
raise ConfigurationError(f"Cannot interpret record: {item!r}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def coerce_records(records: list) -> list[MemoryRecord]:
|
|
58
|
+
"""Turn a list of records or dicts into `MemoryRecord` objects.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
records: Each item is passed to `coerce_record`.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
One `MemoryRecord` per item, in the same order.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ConfigurationError: Any item is not a record or a dict.
|
|
68
|
+
"""
|
|
69
|
+
return [coerce_record(item) for item in records]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = ["coerce_candidate", "coerce_record", "coerce_records"]
|
mimvo/_version.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""The installed Mimvo version, in a module with no imports.
|
|
2
|
+
|
|
3
|
+
Reports stamp this onto their output. Keeping it here lets the models
|
|
4
|
+
import it without importing the whole package.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
__version__ = "0.2.0"
|
|
10
|
+
|
|
11
|
+
__all__ = ["__version__"]
|
mimvo/checks/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Built-in checks and `default_checks`.
|
|
2
|
+
|
|
3
|
+
`default_checks()` returns the standard pipeline: secrets, injection, then
|
|
4
|
+
poisoning. Each one uses an offline detector unless you pass your own.
|
|
5
|
+
|
|
6
|
+
Pass a configured `InjectionCheck`, `PoisoningCheck`, or `SecretsCheck` in
|
|
7
|
+
`Mimvo(checks=[...])` to replace the default check of the same name.
|
|
8
|
+
A check with a new name is added alongside the defaults.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from .base import MemoryCheck, Phase
|
|
14
|
+
from .security import (
|
|
15
|
+
BaseDetector,
|
|
16
|
+
Detection,
|
|
17
|
+
Detector,
|
|
18
|
+
InjectionCheck,
|
|
19
|
+
PoisoningCheck,
|
|
20
|
+
SecretsCheck,
|
|
21
|
+
SecurityCheck,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def default_checks() -> list[MemoryCheck]:
|
|
26
|
+
"""Return the checks `Mimvo()` runs, in order.
|
|
27
|
+
|
|
28
|
+
The order is secrets, then injection, then poisoning. Each check uses
|
|
29
|
+
its offline detector, so this list needs no network and no model download.
|
|
30
|
+
The same input always produces the same findings.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
A new list of check instances. Call it again for a fresh list; do
|
|
34
|
+
not share one list across clients.
|
|
35
|
+
"""
|
|
36
|
+
return [
|
|
37
|
+
SecretsCheck(),
|
|
38
|
+
InjectionCheck(),
|
|
39
|
+
PoisoningCheck(),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"BaseDetector",
|
|
45
|
+
"Detection",
|
|
46
|
+
"Detector",
|
|
47
|
+
"InjectionCheck",
|
|
48
|
+
"MemoryCheck",
|
|
49
|
+
"Phase",
|
|
50
|
+
"PoisoningCheck",
|
|
51
|
+
"SecretsCheck",
|
|
52
|
+
"SecurityCheck",
|
|
53
|
+
"default_checks",
|
|
54
|
+
]
|
mimvo/checks/base.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""The `MemoryCheck` base class and the contract the engine runs it through.
|
|
2
|
+
|
|
3
|
+
A check looks at one memory and returns findings. The simplest check sets
|
|
4
|
+
`name` and implements `check`. Adding a check does not require editing the
|
|
5
|
+
engine: the engine only uses the methods defined here.
|
|
6
|
+
|
|
7
|
+
A scan reads the store once, as a stream, then runs a second pass over the
|
|
8
|
+
packed corpus for work that compares records with each other. A check says
|
|
9
|
+
which passes it needs (`phases`), records what it saw on each pass
|
|
10
|
+
(`observe`), and turns those observations into findings once its last pass
|
|
11
|
+
is done (`conclude`). The defaults wrap `check`, so a plain check never
|
|
12
|
+
has to think about passes; `SecurityCheck` overrides them to split its
|
|
13
|
+
detectors across the passes and vote over both.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Sequence
|
|
19
|
+
from enum import StrEnum
|
|
20
|
+
|
|
21
|
+
from ..context import CheckContext
|
|
22
|
+
from ..models.finding import Finding
|
|
23
|
+
from ..models.memory import MemoryCandidate
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Phase(StrEnum):
|
|
27
|
+
"""The two passes of a scan.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
STREAM: While records are read, one at a time. Only the record
|
|
31
|
+
itself (and records already read) can be seen.
|
|
32
|
+
CORPUS: After every record has been read. `context.existing` and
|
|
33
|
+
the corpus hold the whole scan.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
STREAM = "stream"
|
|
37
|
+
CORPUS = "corpus"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MemoryCheck:
|
|
41
|
+
"""One named check over a single memory.
|
|
42
|
+
|
|
43
|
+
Set `name` and implement `check`. The built-in security checks subclass
|
|
44
|
+
`SecurityCheck`, which subclasses this.
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
name: Label stored on each finding this check emits. The client uses
|
|
48
|
+
it to replace a default check: a custom check named `"injection"`
|
|
49
|
+
takes the place of the built-in injection check.
|
|
50
|
+
needs_corpus: Whether `check` reads other records. `True` (or
|
|
51
|
+
unset) runs it once every record has been read, so
|
|
52
|
+
`context.existing` is the whole scan; `False` runs it while
|
|
53
|
+
records stream in.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
name: str = "base"
|
|
57
|
+
needs_corpus: bool | None = None
|
|
58
|
+
|
|
59
|
+
def check(self, candidate: MemoryCandidate, context: CheckContext) -> list[Finding]:
|
|
60
|
+
"""Inspect one memory and return every problem found.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
candidate: The memory being scanned. Read `candidate.content`.
|
|
64
|
+
context: Scan settings, the clock, neighbouring records, and the
|
|
65
|
+
optional retrieval query.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Findings for this memory. An empty list means this check found
|
|
69
|
+
nothing. Never return `None`.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
NotImplementedError: Always, unless a subclass implements this.
|
|
73
|
+
"""
|
|
74
|
+
raise NotImplementedError
|
|
75
|
+
|
|
76
|
+
# -- engine contract ---------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
def phases(self) -> frozenset[Phase]:
|
|
79
|
+
"""Which passes this check runs in.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
`{CORPUS}` unless `needs_corpus` is `False`, then `{STREAM}`.
|
|
83
|
+
A check that returns both is observed on each pass and
|
|
84
|
+
concluded after the corpus pass.
|
|
85
|
+
"""
|
|
86
|
+
return frozenset({Phase.STREAM if self.needs_corpus is False else Phase.CORPUS})
|
|
87
|
+
|
|
88
|
+
def observe(self, candidate: MemoryCandidate, context: CheckContext, phase: Phase) -> object:
|
|
89
|
+
"""Look at one memory during one pass.
|
|
90
|
+
|
|
91
|
+
The engine keeps whatever this returns and hands every observation
|
|
92
|
+
for the record to `conclude` after the check's last pass.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
candidate: The memory being scanned.
|
|
96
|
+
context: Scan context.
|
|
97
|
+
phase: The pass being run. Always one of `phases()`.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
An observation only this check needs to understand. The default
|
|
101
|
+
is the list of findings `check` returned. An empty (falsy)
|
|
102
|
+
observation means "nothing seen": the engine may drop it rather
|
|
103
|
+
than keep it until the corpus pass.
|
|
104
|
+
"""
|
|
105
|
+
return self.check(candidate, context)
|
|
106
|
+
|
|
107
|
+
def observe_batch(
|
|
108
|
+
self,
|
|
109
|
+
candidates: Sequence[MemoryCandidate],
|
|
110
|
+
context: CheckContext,
|
|
111
|
+
phase: Phase,
|
|
112
|
+
) -> list[object]:
|
|
113
|
+
"""Look at several memories during one pass.
|
|
114
|
+
|
|
115
|
+
The engine hands each pass to checks in batches of
|
|
116
|
+
`Config.batch_size` records, so a check whose work batches well,
|
|
117
|
+
such as a model forward pass, can do it once per batch.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
candidates: The memories, in scan order.
|
|
121
|
+
context: Scan context.
|
|
122
|
+
phase: The pass being run.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
One observation per candidate, in the same order. The default
|
|
126
|
+
calls `observe` on each.
|
|
127
|
+
"""
|
|
128
|
+
return [self.observe(candidate, context, phase) for candidate in candidates]
|
|
129
|
+
|
|
130
|
+
def conclude(self, observations: Sequence[object]) -> list[Finding]:
|
|
131
|
+
"""Turn one record's observations into findings.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
observations: What `observe` returned for this record, one per
|
|
135
|
+
pass the check ran in, in pass order.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
The record's findings. The default joins the finding lists.
|
|
139
|
+
The engine stamps this check's `name` on any finding whose
|
|
140
|
+
`check` is unset.
|
|
141
|
+
"""
|
|
142
|
+
findings: list[Finding] = []
|
|
143
|
+
for observation in observations:
|
|
144
|
+
if isinstance(observation, list):
|
|
145
|
+
findings.extend(f for f in observation if isinstance(f, Finding))
|
|
146
|
+
return findings
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
__all__ = ["MemoryCheck", "Phase"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Security checks: injection, poisoning, and secrets.
|
|
2
|
+
|
|
3
|
+
Layout:
|
|
4
|
+
|
|
5
|
+
security/
|
|
6
|
+
base.py SecurityCheck, Detector, Detection
|
|
7
|
+
injection/ InjectionCheck and one detector per method
|
|
8
|
+
poisoning/ PoisoningCheck and one detector per method
|
|
9
|
+
secrets/ SecretsCheck and one detector per method
|
|
10
|
+
|
|
11
|
+
Each check runs detectors and turns their hits into findings. The defaults
|
|
12
|
+
are offline heuristics. Model and hosted detectors are optional.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
Replace the default injection check with heuristic plus Prompt Guard::
|
|
16
|
+
|
|
17
|
+
from mimvo import Mimvo
|
|
18
|
+
from mimvo.checks.security import InjectionCheck
|
|
19
|
+
from mimvo.checks.security.injection import (
|
|
20
|
+
HeuristicInjectionDetector, PromptGuardDetector,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
guard = Mimvo(checks=[
|
|
24
|
+
InjectionCheck(detectors=[HeuristicInjectionDetector(), PromptGuardDetector()]),
|
|
25
|
+
])
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from .base import BaseDetector, Detection, Detector, SecurityCheck
|
|
31
|
+
from .injection import InjectionCheck
|
|
32
|
+
from .poisoning import PoisoningCheck
|
|
33
|
+
from .secrets import SecretsCheck
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"BaseDetector",
|
|
37
|
+
"Detection",
|
|
38
|
+
"Detector",
|
|
39
|
+
"InjectionCheck",
|
|
40
|
+
"PoisoningCheck",
|
|
41
|
+
"SecretsCheck",
|
|
42
|
+
"SecurityCheck",
|
|
43
|
+
]
|