hanuscode 1.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.
- hanus/__init__.py +5 -0
- hanus/__main__.py +10 -0
- hanus/action_handlers.py +76 -0
- hanus/action_parser.py +82 -0
- hanus/agent_runner.py +1445 -0
- hanus/analysis/__init__.py +5 -0
- hanus/analysis/debt.py +702 -0
- hanus/analysis/dependencies.py +475 -0
- hanus/cache/__init__.py +5 -0
- hanus/cache/response_cache.py +560 -0
- hanus/config.py +401 -0
- hanus/connectors/__init__.py +19 -0
- hanus/connectors/base.py +114 -0
- hanus/connectors/claude_connector.py +146 -0
- hanus/connectors/gemini_connector.py +141 -0
- hanus/connectors/glm_connector.py +160 -0
- hanus/connectors/ollama_connector.py +174 -0
- hanus/connectors/openai_connector.py +122 -0
- hanus/connectors/registry.py +26 -0
- hanus/context/__init__.py +7 -0
- hanus/context/manager.py +837 -0
- hanus/context/selective.py +626 -0
- hanus/error_recovery/__init__.py +5 -0
- hanus/error_recovery/auto_fix.py +605 -0
- hanus/hooks/__init__.py +5 -0
- hanus/hooks/manager.py +247 -0
- hanus/instincts/__init__.py +44 -0
- hanus/instincts/cli.py +372 -0
- hanus/instincts/detector.py +281 -0
- hanus/instincts/evolver.py +361 -0
- hanus/instincts/manager.py +343 -0
- hanus/instincts/types.py +253 -0
- hanus/logger.py +81 -0
- hanus/memory/__init__.py +8 -0
- hanus/memory/manager.py +265 -0
- hanus/memory/types.py +119 -0
- hanus/monitor.py +341 -0
- hanus/parallel/__init__.py +5 -0
- hanus/parallel/executor.py +300 -0
- hanus/permissions.py +182 -0
- hanus/plan/__init__.py +8 -0
- hanus/plan/mode.py +267 -0
- hanus/plan/models.py +152 -0
- hanus/plugin_manager.py +754 -0
- hanus/plugin_registry.py +391 -0
- hanus/plugins/__init__.py +1 -0
- hanus/plugins/arena.py +630 -0
- hanus/plugins/code_review.py +123 -0
- hanus/plugins/cortex.py +1750 -0
- hanus/plugins/deps_check.py +27 -0
- hanus/plugins/git_ops.py +33 -0
- hanus/plugins/metasploit.py +530 -0
- hanus/plugins/notes.py +583 -0
- hanus/plugins/search_code.py +59 -0
- hanus/plugins/searchsploit.py +495 -0
- hanus/plugins/strategist.py +175 -0
- hanus/plugins/webui.py +5200 -0
- hanus/profiles.py +479 -0
- hanus/profiles_builtin/__init__.py +0 -0
- hanus/profiles_builtin/architect/profile.yaml +12 -0
- hanus/profiles_builtin/architect/system_prompt.txt +71 -0
- hanus/profiles_builtin/deep/profile.yaml +12 -0
- hanus/profiles_builtin/deep/system_prompt.txt +66 -0
- hanus/profiles_builtin/developer/__init__.py +0 -0
- hanus/profiles_builtin/developer/profile.yaml +9 -0
- hanus/profiles_builtin/developer/system_prompt.txt +176 -0
- hanus/profiles_builtin/speed/profile.yaml +12 -0
- hanus/profiles_builtin/speed/system_prompt.txt +51 -0
- hanus/project_tools.py +177 -0
- hanus/query_engine.py +1594 -0
- hanus/rules/__init__.py +237 -0
- hanus/search/__init__.py +5 -0
- hanus/search/semantic.py +596 -0
- hanus/session_manager.py +547 -0
- hanus/skill_manager.py +702 -0
- hanus/skills/__init__.py +4 -0
- hanus/subagent/__init__.py +8 -0
- hanus/subagent/agents/__init__.py +253 -0
- hanus/subagent/manager.py +309 -0
- hanus/subagent/types.py +266 -0
- hanus/suggestions/__init__.py +5 -0
- hanus/suggestions/proactive.py +451 -0
- hanus/tasks/__init__.py +8 -0
- hanus/tasks/manager.py +330 -0
- hanus/tasks/models.py +106 -0
- hanus/terminal_prompt.py +166 -0
- hanus/tools.py +1849 -0
- hanus/ui.py +939 -0
- hanuscode-1.0.0.dist-info/METADATA +1151 -0
- hanuscode-1.0.0.dist-info/RECORD +93 -0
- hanuscode-1.0.0.dist-info/WHEEL +5 -0
- hanuscode-1.0.0.dist-info/entry_points.txt +2 -0
- hanuscode-1.0.0.dist-info/top_level.txt +1 -0
hanus/plugins/cortex.py
ADDED
|
@@ -0,0 +1,1750 @@
|
|
|
1
|
+
# plugins/cortex.py ā Semantic Memory System with Knowledge Graph
|
|
2
|
+
"""
|
|
3
|
+
Semantic memory system with knowledge graphs.
|
|
4
|
+
|
|
5
|
+
Allows long-term information retention and understanding relationships between concepts
|
|
6
|
+
(people, events, preferences, projects) rather than just text matching.
|
|
7
|
+
|
|
8
|
+
Commands:
|
|
9
|
+
/cortex remember <entity> <type> <attributes> ā Save entity with attributes
|
|
10
|
+
/cortex relate <entity1> <relation> <entity2> ā Create relation
|
|
11
|
+
/cortex recall <entity> ā Get entity info
|
|
12
|
+
/cortex find <type> ā Search entities by type
|
|
13
|
+
/cortex query <entity> <depth> ā Query graph (relations)
|
|
14
|
+
/cortex forget <entity> ā Delete entity
|
|
15
|
+
/cortex list ā List entities
|
|
16
|
+
/cortex types ā View entity types
|
|
17
|
+
/cortex relations ā View relation types
|
|
18
|
+
/cortex graph [entity] ā Visualize graph
|
|
19
|
+
/cortex timeline <entity> ā View entity events
|
|
20
|
+
/cortex similar <entity> ā Similar entities
|
|
21
|
+
/cortex path <entity1> <entity2> ā Path between entities
|
|
22
|
+
"""
|
|
23
|
+
import json
|
|
24
|
+
import re
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
from typing import Dict, List, Set, Optional, Tuple
|
|
28
|
+
from collections import defaultdict
|
|
29
|
+
|
|
30
|
+
NAME = "cortex"
|
|
31
|
+
DESCRIPTION = "Semantic memory system with knowledge graphs - USE THIS to store data you learn"
|
|
32
|
+
USAGE = "remember|relate|recall|find|query|forget|list|types|relations|graph|timeline|similar|path"
|
|
33
|
+
|
|
34
|
+
AGENT_DOC = """
|
|
35
|
+
## ā ļø IMPORTANT: Cortex is for STORING DATA - DO NOT create files in .cortex/
|
|
36
|
+
|
|
37
|
+
Cortex is a KNOWLEDGE GRAPH MEMORY SYSTEM. When you learn or discover information,
|
|
38
|
+
STORE IT HERE using the commands below. DO NOT create markdown files in .cortex/ directory.
|
|
39
|
+
|
|
40
|
+
## Why Cortex?
|
|
41
|
+
|
|
42
|
+
**Notes** = text notes with keyword search.
|
|
43
|
+
**Cortex** = STRUCTURED MEMORY with:
|
|
44
|
+
- Typed entities (person, target, vulnerability, codebase, etc.)
|
|
45
|
+
- Typed relations between entities (with automatic inverse relations)
|
|
46
|
+
- Graph queries (find related entities, paths, similar items)
|
|
47
|
+
- Episodic memory (events with temporal context)
|
|
48
|
+
|
|
49
|
+
## MANDATORY - When to Use Cortex
|
|
50
|
+
|
|
51
|
+
**You MUST use Cortex after EVERY significant action:**
|
|
52
|
+
|
|
53
|
+
### After Security Work (pentesting, CTF, recon):
|
|
54
|
+
1. **Found target/host?** ā `<run_plugin name="cortex" args="remember target1 target type=host ip=10.10.10.1 os=linux"/>`
|
|
55
|
+
2. **Found open port?** ā `<run_plugin name="cortex" args="remember port_80 service port=80 service=http host=target1"/>`
|
|
56
|
+
3. **Found vulnerability?** ā `<run_plugin name="cortex" args="remember vuln1 vulnerability cve=CVE-2024-1234 severity=high"/>`
|
|
57
|
+
4. **Found credential?** ā `<run_plugin name="cortex" args="remember admin_cred credential type=password user=admin source=target1"/>`
|
|
58
|
+
5. **Exploited something?** ā `<run_plugin name="cortex" args="remember exploit1 exploit type=rce target=target1 cve=CVE-2024-1234"/>`
|
|
59
|
+
|
|
60
|
+
### After Creating/Modifying Code:
|
|
61
|
+
1. **New project?** ā `<run_plugin name="cortex" args="remember myproject project language=python framework=flask"/>`
|
|
62
|
+
2. **New file?** ā `<run_plugin name="cortex" args="remember auth_module module path=auth.py purpose=authentication"/>`
|
|
63
|
+
3. **New API?** ā `<run_plugin name="cortex" args="remember login_api api method=POST path=/login auth=jwt"/>`
|
|
64
|
+
|
|
65
|
+
### Create Relations:
|
|
66
|
+
```
|
|
67
|
+
<run_plugin name="cortex" args="relate target1 has_vuln vuln1"/>
|
|
68
|
+
<run_plugin name="cortex" args="relate vuln1 exploited_by exploit1"/>
|
|
69
|
+
<run_plugin name="cortex" args="relate auth_module defines login_api"/>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quick Reference Commands
|
|
73
|
+
|
|
74
|
+
### remember - Save an entity
|
|
75
|
+
```
|
|
76
|
+
<run_plugin name="cortex" args="remember <entity_name> <type> [key=value ...]"/>
|
|
77
|
+
<run_plugin name="cortex" args="remember webserver host ip=192.168.1.1 os=linux ports=22,80,443"/>
|
|
78
|
+
<run_plugin name="cortex" args="remember sqli_vuln vulnerability severity=high url=/search param=id"/>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### relate - Create relationship
|
|
82
|
+
```
|
|
83
|
+
<run_plugin name="cortex" args="relate <entity1> <relation> <entity2>"/>
|
|
84
|
+
<run_plugin name="cortex" args="relate webserver has_vuln sqli_vuln"/>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### recall - Get entity info
|
|
88
|
+
```
|
|
89
|
+
<run_plugin name="cortex" args="recall webserver"/>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### find - Search by type
|
|
93
|
+
```
|
|
94
|
+
<run_plugin name="cortex" args="find host"/>
|
|
95
|
+
<run_plugin name="cortex" args="find vulnerability severity=high"/>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### query - Get related entities
|
|
99
|
+
```
|
|
100
|
+
<run_plugin name="cortex" args="query webserver 2"/>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### list - List all entities
|
|
104
|
+
```
|
|
105
|
+
<run_plugin name="cortex" args="list"/>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Entity Types (you can use ANY type)
|
|
109
|
+
|
|
110
|
+
| Type | Use Case | Common Attributes |
|
|
111
|
+
|------|----------|-------------------|
|
|
112
|
+
| target | Security targets | type, scope, status |
|
|
113
|
+
| host | Servers/hosts | ip, hostname, os, ports, services |
|
|
114
|
+
| domain | Domain names | name, registrar, ip |
|
|
115
|
+
| port | Open ports | number, service, version |
|
|
116
|
+
| vulnerability | Vulnerabilities | cve, severity, cvss, affected |
|
|
117
|
+
| exploit | Exploits/PoCs | name, type, cve, reliability |
|
|
118
|
+
| credential | Found credentials | type, source, valid, scope |
|
|
119
|
+
| tool | Security tools | category, purpose, usage |
|
|
120
|
+
| finding | Security findings | severity, type, url, evidence |
|
|
121
|
+
| project | Software projects | status, language, framework |
|
|
122
|
+
| module | Code files | path, purpose, dependencies |
|
|
123
|
+
| api | APIs/endpoints | method, path, auth |
|
|
124
|
+
| person | People | role, team, email |
|
|
125
|
+
|
|
126
|
+
## Relation Types (you can use ANY relation)
|
|
127
|
+
|
|
128
|
+
| Relation | Inverse | Meaning |
|
|
129
|
+
|----------|---------|---------|
|
|
130
|
+
| has_vuln | vuln_in | Has vulnerability |
|
|
131
|
+
| exploits | exploited_by | Exploits vulnerability |
|
|
132
|
+
| targets | targeted_by | Targets host/domain |
|
|
133
|
+
| hosts | hosted_on | Hosts service |
|
|
134
|
+
| contains | contained_in | Contains sub-component |
|
|
135
|
+
| uses | used_by | Uses technology/service |
|
|
136
|
+
| depends_on | depended_by | Depends on |
|
|
137
|
+
| depends_on | depended_by | Depends on |
|
|
138
|
+
| calls | called_by | Calls function |
|
|
139
|
+
| imports | imported_by | Imports module |
|
|
140
|
+
| defines | defined_in | Defines function/class |
|
|
141
|
+
| exposes | exposed_by | Exposes API |
|
|
142
|
+
| fixes | fixed_by | Fixes bug |
|
|
143
|
+
| has_vuln | vuln_in | Has vulnerability |
|
|
144
|
+
| exploits | exploited_by | Exploits vuln |
|
|
145
|
+
| targets | targeted_by | Targets host/domain |
|
|
146
|
+
| finds | found_by | Finds vulnerability |
|
|
147
|
+
| hosts | hosted_on | Hosts service |
|
|
148
|
+
| works_on | has_member | Works on project |
|
|
149
|
+
|
|
150
|
+
## Commands
|
|
151
|
+
|
|
152
|
+
### remember <entity> <type> [key=value...]
|
|
153
|
+
Save entity with attributes.
|
|
154
|
+
|
|
155
|
+
<run_plugin name="cortex" args="remember 'Auth API' project status=active language=python framework=flask"/>
|
|
156
|
+
<run_plugin name="cortex" args="remember auth.py module path=/auth purpose='authentication module'"/>
|
|
157
|
+
<run_plugin name="cortex" args="remember login function file=auth.py params='username,password'"/>
|
|
158
|
+
|
|
159
|
+
### relate <entity1> <relation> <entity2>
|
|
160
|
+
Create relation (auto-creates inverse).
|
|
161
|
+
|
|
162
|
+
<run_plugin name="cortex" args="relate 'Auth API' contains auth.py"/>
|
|
163
|
+
<run_plugin name="cortex" args="relate auth.py defines login"/>
|
|
164
|
+
<run_plugin name="cortex" args="relate 'Auth API' uses Flask"/>
|
|
165
|
+
|
|
166
|
+
### recall <entity>
|
|
167
|
+
Get all entity info.
|
|
168
|
+
|
|
169
|
+
<run_plugin name="cortex" args="recall 'Auth API'"/>
|
|
170
|
+
|
|
171
|
+
### find <type> [key=value]
|
|
172
|
+
Search entities by type and/or attributes.
|
|
173
|
+
|
|
174
|
+
<run_plugin name="cortex" args="find module"/>
|
|
175
|
+
<run_plugin name="cortex" args="find bug severity=critical"/>
|
|
176
|
+
|
|
177
|
+
### query <entity> [depth]
|
|
178
|
+
Get related entities up to N levels.
|
|
179
|
+
|
|
180
|
+
<run_plugin name="cortex" args="query 'Auth API' 2"/>
|
|
181
|
+
|
|
182
|
+
### graph [entity]
|
|
183
|
+
Visualize the knowledge graph.
|
|
184
|
+
|
|
185
|
+
<run_plugin name="cortex" args="graph"/>
|
|
186
|
+
|
|
187
|
+
### reset --force
|
|
188
|
+
Clear all data (use with caution).
|
|
189
|
+
|
|
190
|
+
<run_plugin name="cortex" args="reset --force"/>
|
|
191
|
+
|
|
192
|
+
<run_plugin name="cortex" args="path John Python"/>
|
|
193
|
+
|
|
194
|
+
## Notes
|
|
195
|
+
|
|
196
|
+
- Entity types and relations are SUGGESTIONS, not restrictions
|
|
197
|
+
- You can create any type or relation you need
|
|
198
|
+
- Inverse relations are auto-created when defined
|
|
199
|
+
- Use /cortex types and /cortex relations to see all available
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
DATA_DIR = Path.home() / ".hanus" / "cortex"
|
|
203
|
+
ENTITIES_FILE = DATA_DIR / "entities.json"
|
|
204
|
+
RELATIONS_FILE = DATA_DIR / "relations.json"
|
|
205
|
+
EVENTS_FILE = DATA_DIR / "events.json"
|
|
206
|
+
|
|
207
|
+
# Suggested entity types with common attributes
|
|
208
|
+
# These are SUGGESTIONS - any type can be used
|
|
209
|
+
ENTITY_TYPES = {
|
|
210
|
+
# === PEOPLE & ORGANIZATIONS ===
|
|
211
|
+
"person": {
|
|
212
|
+
"description": "Person",
|
|
213
|
+
"default_attrs": ["role", "team", "email", "seniority", "timezone", "github", "skills"]
|
|
214
|
+
},
|
|
215
|
+
"organization": {
|
|
216
|
+
"description": "Company, team or group",
|
|
217
|
+
"default_attrs": ["type", "size", "industry", "website", "scope"]
|
|
218
|
+
},
|
|
219
|
+
"team": {
|
|
220
|
+
"description": "Work team",
|
|
221
|
+
"default_attrs": ["name", "project", "members", "tech_stack"]
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
# === PROJECTS & CODE ===
|
|
225
|
+
"project": {
|
|
226
|
+
"description": "Software project",
|
|
227
|
+
"default_attrs": ["status", "priority", "language", "framework", "repo", "branch"]
|
|
228
|
+
},
|
|
229
|
+
"codebase": {
|
|
230
|
+
"description": "Code repository",
|
|
231
|
+
"default_attrs": ["language", "framework", "size", "structure", "main_tech"]
|
|
232
|
+
},
|
|
233
|
+
"module": {
|
|
234
|
+
"description": "Code module or component",
|
|
235
|
+
"default_attrs": ["path", "language", "purpose", "dependencies"]
|
|
236
|
+
},
|
|
237
|
+
"function": {
|
|
238
|
+
"description": "Function or method",
|
|
239
|
+
"default_attrs": ["name", "file", "params", "returns", "purpose"]
|
|
240
|
+
},
|
|
241
|
+
"class": {
|
|
242
|
+
"description": "Class or type",
|
|
243
|
+
"default_attrs": ["name", "file", "inherits", "methods", "purpose"]
|
|
244
|
+
},
|
|
245
|
+
"api": {
|
|
246
|
+
"description": "API or service",
|
|
247
|
+
"default_attrs": ["method", "path", "auth", "rate_limit", "purpose"]
|
|
248
|
+
},
|
|
249
|
+
"endpoint": {
|
|
250
|
+
"description": "HTTP endpoint",
|
|
251
|
+
"default_attrs": ["url", "method", "auth_type", "params", "response_type"]
|
|
252
|
+
},
|
|
253
|
+
"database": {
|
|
254
|
+
"description": "Database",
|
|
255
|
+
"default_attrs": ["type", "host", "port", "name", "schema"]
|
|
256
|
+
},
|
|
257
|
+
"dependency": {
|
|
258
|
+
"description": "Dependency or library",
|
|
259
|
+
"default_attrs": ["name", "version", "source", "license", "purpose"]
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
# === DEVELOPMENT ===
|
|
263
|
+
"bug": {
|
|
264
|
+
"description": "Bug or issue",
|
|
265
|
+
"default_attrs": ["severity", "status", "component", "found_in", "fixed_in"]
|
|
266
|
+
},
|
|
267
|
+
"feature": {
|
|
268
|
+
"description": "Feature or functionality",
|
|
269
|
+
"default_attrs": ["status", "priority", "effort", "component", "milestone"]
|
|
270
|
+
},
|
|
271
|
+
"pr": {
|
|
272
|
+
"description": "Pull Request",
|
|
273
|
+
"default_attrs": ["number", "status", "author", "branch", "files_changed"]
|
|
274
|
+
},
|
|
275
|
+
"commit": {
|
|
276
|
+
"description": "Git commit",
|
|
277
|
+
"default_attrs": ["hash", "author", "date", "message", "files"]
|
|
278
|
+
},
|
|
279
|
+
"test": {
|
|
280
|
+
"description": "Test or test suite",
|
|
281
|
+
"default_attrs": ["type", "coverage", "status", "component"]
|
|
282
|
+
},
|
|
283
|
+
"config": {
|
|
284
|
+
"description": "Configuration",
|
|
285
|
+
"default_attrs": ["file", "environment", "purpose", "sensitive"]
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
# === SECURITY - INFRASTRUCTURE ===
|
|
289
|
+
"target": {
|
|
290
|
+
"description": "Security target (host, domain, app)",
|
|
291
|
+
"default_attrs": ["type", "scope", "status", "priority", "notes"]
|
|
292
|
+
},
|
|
293
|
+
"domain": {
|
|
294
|
+
"description": "Web domain",
|
|
295
|
+
"default_attrs": ["name", "registrar", "ip", "ports", "tech_stack"]
|
|
296
|
+
},
|
|
297
|
+
"host": {
|
|
298
|
+
"description": "Host or server",
|
|
299
|
+
"default_attrs": ["ip", "hostname", "os", "ports", "services"]
|
|
300
|
+
},
|
|
301
|
+
"ip": {
|
|
302
|
+
"description": "IP address",
|
|
303
|
+
"default_attrs": ["address", "type", "asn", "country", "hostname"]
|
|
304
|
+
},
|
|
305
|
+
"port": {
|
|
306
|
+
"description": "Open port",
|
|
307
|
+
"default_attrs": ["number", "protocol", "service", "version", "banner"]
|
|
308
|
+
},
|
|
309
|
+
"service": {
|
|
310
|
+
"description": "Detected service",
|
|
311
|
+
"default_attrs": ["name", "port", "version", "product", "vulnerabilities"]
|
|
312
|
+
},
|
|
313
|
+
"network": {
|
|
314
|
+
"description": "Network or subnet",
|
|
315
|
+
"default_attrs": ["cidr", "gateway", "range", "hosts", "purpose"]
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
# === SECURITY - VULNERABILITIES ===
|
|
319
|
+
"vulnerability": {
|
|
320
|
+
"description": "Vulnerability",
|
|
321
|
+
"default_attrs": ["cve", "severity", "cvss", "vector", "affected"]
|
|
322
|
+
},
|
|
323
|
+
"cve": {
|
|
324
|
+
"description": "Specific CVE",
|
|
325
|
+
"default_attrs": ["id", "severity", "product", "version", "exploit_available"]
|
|
326
|
+
},
|
|
327
|
+
"exploit": {
|
|
328
|
+
"description": "Exploit or PoC",
|
|
329
|
+
"default_attrs": ["name", "type", "cve", "reliability", "source"]
|
|
330
|
+
},
|
|
331
|
+
"payload": {
|
|
332
|
+
"description": "Exploit payload",
|
|
333
|
+
"default_attrs": ["type", "language", "target", "purpose", "detected_by"]
|
|
334
|
+
},
|
|
335
|
+
"finding": {
|
|
336
|
+
"description": "Security finding",
|
|
337
|
+
"default_attrs": ["severity", "type", "url", "parameter", "evidence"]
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
# === SECURITY - TOOLS & TECHNIQUES ===
|
|
341
|
+
"tool": {
|
|
342
|
+
"description": "Security tool",
|
|
343
|
+
"default_attrs": ["category", "language", "source", "purpose", "usage"]
|
|
344
|
+
},
|
|
345
|
+
"technique": {
|
|
346
|
+
"description": "Attack/defense technique",
|
|
347
|
+
"default_attrs": ["category", "mitre_id", "difficulty", "detection", "mitigation"]
|
|
348
|
+
},
|
|
349
|
+
"wordlist": {
|
|
350
|
+
"description": "Wordlist or dictionary",
|
|
351
|
+
"default_attrs": ["type", "size", "source", "purpose"]
|
|
352
|
+
},
|
|
353
|
+
"payload_type": {
|
|
354
|
+
"description": "Payload type",
|
|
355
|
+
"default_attrs": ["category", "context", "encoding", "bypasses"]
|
|
356
|
+
},
|
|
357
|
+
"scan": {
|
|
358
|
+
"description": "Scan performed",
|
|
359
|
+
"default_attrs": ["type", "tool", "target", "date", "findings"]
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
# === SECURITY - CREDENTIALS & ACCESS ===
|
|
363
|
+
"credential": {
|
|
364
|
+
"description": "Found credential",
|
|
365
|
+
"default_attrs": ["type", "source", "valid", "scope", "leaked_in"]
|
|
366
|
+
},
|
|
367
|
+
"session": {
|
|
368
|
+
"description": "Session or token",
|
|
369
|
+
"default_attrs": ["type", "valid_until", "scope", "user"]
|
|
370
|
+
},
|
|
371
|
+
"backdoor": {
|
|
372
|
+
"description": "Backdoor or access",
|
|
373
|
+
"default_attrs": ["type", "location", "persistence", "detected"]
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
# === GENERAL ===
|
|
377
|
+
"event": {
|
|
378
|
+
"description": "Event or meeting",
|
|
379
|
+
"default_attrs": ["date", "time", "location", "outcome", "attendees"]
|
|
380
|
+
},
|
|
381
|
+
"preference": {
|
|
382
|
+
"description": "Preference or setting",
|
|
383
|
+
"default_attrs": ["category", "value", "context", "reason"]
|
|
384
|
+
},
|
|
385
|
+
"technology": {
|
|
386
|
+
"description": "General technology",
|
|
387
|
+
"default_attrs": ["type", "version", "category", "maturity", "docs"]
|
|
388
|
+
},
|
|
389
|
+
"location": {
|
|
390
|
+
"description": "Location",
|
|
391
|
+
"default_attrs": ["type", "address", "timezone", "related_hosts"]
|
|
392
|
+
},
|
|
393
|
+
"task": {
|
|
394
|
+
"description": "Pending task",
|
|
395
|
+
"default_attrs": ["status", "priority", "due", "assignee", "context"]
|
|
396
|
+
},
|
|
397
|
+
"decision": {
|
|
398
|
+
"description": "Decision made",
|
|
399
|
+
"default_attrs": ["date", "context", "outcome", "rationale", "impact"]
|
|
400
|
+
},
|
|
401
|
+
"resource": {
|
|
402
|
+
"description": "External resource",
|
|
403
|
+
"default_attrs": ["type", "url", "access", "purpose"]
|
|
404
|
+
},
|
|
405
|
+
"note": {
|
|
406
|
+
"description": "Note or information",
|
|
407
|
+
"default_attrs": ["source", "date", "importance", "context"]
|
|
408
|
+
},
|
|
409
|
+
"knowledge": {
|
|
410
|
+
"description": "Learned knowledge",
|
|
411
|
+
"default_attrs": ["topic", "source", "confidence", "applicable_to"]
|
|
412
|
+
},
|
|
413
|
+
"concept": {
|
|
414
|
+
"description": "Concept or idea",
|
|
415
|
+
"default_attrs": ["definition", "related_to", "importance", "context"]
|
|
416
|
+
},
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
# Relation types with inverse mapping
|
|
420
|
+
# These are SUGGESTIONS - any relation can be used
|
|
421
|
+
# Format: relation_name -> {inverse: inverse_relation, description: meaning}
|
|
422
|
+
RELATION_TYPES = {
|
|
423
|
+
# === SOCIAL ===
|
|
424
|
+
"knows": {"inverse": "knows", "description": "Knows"},
|
|
425
|
+
"works_with": {"inverse": "works_with", "description": "Works with"},
|
|
426
|
+
"reports_to": {"inverse": "manages", "description": "Reports to"},
|
|
427
|
+
"manages": {"inverse": "reports_to", "description": "Manages"},
|
|
428
|
+
"collaborates_with": {"inverse": "collaborates_with", "description": "Collaborates with"},
|
|
429
|
+
|
|
430
|
+
# === PROJECTS ===
|
|
431
|
+
"works_on": {"inverse": "has_member", "description": "Works on"},
|
|
432
|
+
"has_member": {"inverse": "works_on", "description": "Has member"},
|
|
433
|
+
"leads": {"inverse": "led_by", "description": "Leads"},
|
|
434
|
+
"led_by": {"inverse": "leads", "description": "Led by"},
|
|
435
|
+
"owns": {"inverse": "owned_by", "description": "Owns"},
|
|
436
|
+
"owned_by": {"inverse": "owns", "description": "Owned by"},
|
|
437
|
+
"maintains": {"inverse": "maintained_by", "description": "Maintains"},
|
|
438
|
+
"maintained_by": {"inverse": "maintains", "description": "Maintained by"},
|
|
439
|
+
|
|
440
|
+
# === CODE ===
|
|
441
|
+
"imports": {"inverse": "imported_by", "description": "Imports"},
|
|
442
|
+
"imported_by": {"inverse": "imports", "description": "Imported by"},
|
|
443
|
+
"calls": {"inverse": "called_by", "description": "Calls"},
|
|
444
|
+
"called_by": {"inverse": "calls", "description": "Called by"},
|
|
445
|
+
"extends": {"inverse": "extended_by", "description": "Extends"},
|
|
446
|
+
"extended_by": {"inverse": "extends", "description": "Extended by"},
|
|
447
|
+
"implements": {"inverse": "implemented_by", "description": "Implements"},
|
|
448
|
+
"implemented_by": {"inverse": "implements", "description": "Implemented by"},
|
|
449
|
+
"overrides": {"inverse": "overridden_by", "description": "Overrides"},
|
|
450
|
+
"overridden_by": {"inverse": "overrides", "description": "Overridden by"},
|
|
451
|
+
"contains": {"inverse": "contained_in", "description": "Contains"},
|
|
452
|
+
"contained_in": {"inverse": "contains", "description": "Contained in"},
|
|
453
|
+
"defines": {"inverse": "defined_in", "description": "Defines"},
|
|
454
|
+
"defined_in": {"inverse": "defines", "description": "Defined in"},
|
|
455
|
+
"references": {"inverse": "referenced_by", "description": "References"},
|
|
456
|
+
"referenced_by": {"inverse": "references", "description": "Referenced by"},
|
|
457
|
+
"exposes": {"inverse": "exposed_by", "description": "Exposes"},
|
|
458
|
+
"exposed_by": {"inverse": "exposes", "description": "Exposed by"},
|
|
459
|
+
|
|
460
|
+
# === DEPENDENCIES ===
|
|
461
|
+
"depends_on": {"inverse": "depended_by", "description": "Depends on"},
|
|
462
|
+
"depended_by": {"inverse": "depends_on", "description": "Depended by"},
|
|
463
|
+
"requires": {"inverse": "required_by", "description": "Requires"},
|
|
464
|
+
"required_by": {"inverse": "requires", "description": "Required by"},
|
|
465
|
+
"uses": {"inverse": "used_by", "description": "Uses"},
|
|
466
|
+
"used_by": {"inverse": "uses", "description": "Used by"},
|
|
467
|
+
"includes": {"inverse": "included_in", "description": "Includes"},
|
|
468
|
+
"included_in": {"inverse": "includes", "description": "Included in"},
|
|
469
|
+
|
|
470
|
+
# === DEVELOPMENT ===
|
|
471
|
+
"fixes": {"inverse": "fixed_by", "description": "Fixes"},
|
|
472
|
+
"fixed_by": {"inverse": "fixes", "description": "Fixed by"},
|
|
473
|
+
"introduces": {"inverse": "introduced_by", "description": "Introduces"},
|
|
474
|
+
"introduced_by": {"inverse": "introduces", "description": "Introduced by"},
|
|
475
|
+
"causes": {"inverse": "caused_by", "description": "Causes"},
|
|
476
|
+
"caused_by": {"inverse": "causes", "description": "Caused by"},
|
|
477
|
+
"affects": {"inverse": "affected_by", "description": "Affects"},
|
|
478
|
+
"affected_by": {"inverse": "affects", "description": "Affected by"},
|
|
479
|
+
"tests": {"inverse": "tested_by", "description": "Tests"},
|
|
480
|
+
"tested_by": {"inverse": "tests", "description": "Tested by"},
|
|
481
|
+
"covers": {"inverse": "covered_by", "description": "Covers"},
|
|
482
|
+
"covered_by": {"inverse": "covers", "description": "Covered by"},
|
|
483
|
+
"merges": {"inverse": "merged_in", "description": "Merges"},
|
|
484
|
+
"merged_in": {"inverse": "merges", "description": "Merged in"},
|
|
485
|
+
|
|
486
|
+
# === SECURITY - INFRASTRUCTURE ===
|
|
487
|
+
"hosts": {"inverse": "hosted_on", "description": "Hosts"},
|
|
488
|
+
"hosted_on": {"inverse": "hosts", "description": "Hosted on"},
|
|
489
|
+
"runs_on": {"inverse": "runs", "description": "Runs on"},
|
|
490
|
+
"runs": {"inverse": "runs_on", "description": "Runs"},
|
|
491
|
+
"has_port": {"inverse": "port_of", "description": "Has port"},
|
|
492
|
+
"port_of": {"inverse": "has_port", "description": "Port of"},
|
|
493
|
+
"has_service": {"inverse": "service_of", "description": "Has service"},
|
|
494
|
+
"service_of": {"inverse": "has_service", "description": "Service of"},
|
|
495
|
+
"resolves_to": {"inverse": "resolves_from", "description": "Resolves to"},
|
|
496
|
+
"resolves_from": {"inverse": "resolves_to", "description": "Resolves from"},
|
|
497
|
+
"belongs_to": {"inverse": "owns", "description": "Belongs to"},
|
|
498
|
+
"part_of_network": {"inverse": "has_host", "description": "Part of network"},
|
|
499
|
+
"has_host": {"inverse": "part_of_network", "description": "Has host"},
|
|
500
|
+
|
|
501
|
+
# === SECURITY - ATTACKS ===
|
|
502
|
+
"has_vuln": {"inverse": "vuln_in", "description": "Has vulnerability"},
|
|
503
|
+
"vuln_in": {"inverse": "has_vuln", "description": "Vulnerability in"},
|
|
504
|
+
"exploits": {"inverse": "exploited_by", "description": "Exploits"},
|
|
505
|
+
"exploited_by": {"inverse": "exploits", "description": "Exploited by"},
|
|
506
|
+
"targets": {"inverse": "targeted_by", "description": "Targets"},
|
|
507
|
+
"targeted_by": {"inverse": "targets", "description": "Targeted by"},
|
|
508
|
+
"attacks": {"inverse": "attacked_by", "description": "Attacks"},
|
|
509
|
+
"attacked_by": {"inverse": "attacks", "description": "Attacked by"},
|
|
510
|
+
"scans": {"inverse": "scanned_by", "description": "Scans"},
|
|
511
|
+
"scanned_by": {"inverse": "scans", "description": "Scanned by"},
|
|
512
|
+
"finds": {"inverse": "found_by", "description": "Finds"},
|
|
513
|
+
"found_by": {"inverse": "finds", "description": "Found by"},
|
|
514
|
+
"detects": {"inverse": "detected_by", "description": "Detects"},
|
|
515
|
+
"detected_by": {"inverse": "detects", "description": "Detected by"},
|
|
516
|
+
"bypasses": {"inverse": "bypassed_by", "description": "Bypasses"},
|
|
517
|
+
"bypassed_by": {"inverse": "bypasses", "description": "Bypassed by"},
|
|
518
|
+
"mitigates": {"inverse": "mitigated_by", "description": "Mitigates"},
|
|
519
|
+
"mitigated_by": {"inverse": "mitigates", "description": "Mitigated by"},
|
|
520
|
+
"patches": {"inverse": "patched_by", "description": "Patches"},
|
|
521
|
+
"patched_by": {"inverse": "patches", "description": "Patched by"},
|
|
522
|
+
|
|
523
|
+
# === SECURITY - CREDENTIALS ===
|
|
524
|
+
"leaks": {"inverse": "leaked_in", "description": "Leaks"},
|
|
525
|
+
"leaked_in": {"inverse": "leaks", "description": "Leaked in"},
|
|
526
|
+
"uses_credential": {"inverse": "credential_for", "description": "Uses credential"},
|
|
527
|
+
"credential_for": {"inverse": "uses_credential", "description": "Credential for"},
|
|
528
|
+
"valid_for": {"inverse": "has_valid", "description": "Valid for"},
|
|
529
|
+
"has_valid": {"inverse": "valid_for", "description": "Has valid"},
|
|
530
|
+
"compromises": {"inverse": "compromised_by", "description": "Compromises"},
|
|
531
|
+
"compromised_by": {"inverse": "compromises", "description": "Compromised by"},
|
|
532
|
+
|
|
533
|
+
# === SECURITY - TOOLS ===
|
|
534
|
+
"runs_tool": {"inverse": "tool_runs_on", "description": "Runs tool"},
|
|
535
|
+
"tool_runs_on": {"inverse": "runs_tool", "description": "Tool runs on"},
|
|
536
|
+
"generates": {"inverse": "generated_by", "description": "Generates"},
|
|
537
|
+
"generated_by": {"inverse": "generates", "description": "Generated by"},
|
|
538
|
+
"produces": {"inverse": "produced_by", "description": "Produces"},
|
|
539
|
+
"produced_by": {"inverse": "produces", "description": "Produced by"},
|
|
540
|
+
"consumes": {"inverse": "consumed_by", "description": "Consumes"},
|
|
541
|
+
"consumed_by": {"inverse": "consumes", "description": "Consumed by"},
|
|
542
|
+
|
|
543
|
+
# === STRUCTURAL ===
|
|
544
|
+
"related_to": {"inverse": "related_to", "description": "Related to"},
|
|
545
|
+
"part_of": {"inverse": "has_part", "description": "Part of"},
|
|
546
|
+
"has_part": {"inverse": "part_of", "description": "Has part"},
|
|
547
|
+
"parent_of": {"inverse": "child_of", "description": "Parent of"},
|
|
548
|
+
"child_of": {"inverse": "parent_of", "description": "Child of"},
|
|
549
|
+
"similar_to": {"inverse": "similar_to", "description": "Similar to"},
|
|
550
|
+
"same_as": {"inverse": "same_as", "description": "Same as"},
|
|
551
|
+
"alternative_to": {"inverse": "alternative_to", "description": "Alternative to"},
|
|
552
|
+
|
|
553
|
+
# === CREATION ===
|
|
554
|
+
"created": {"inverse": "created_by", "description": "Created"},
|
|
555
|
+
"created_by": {"inverse": "created", "description": "Created by"},
|
|
556
|
+
"modified": {"inverse": "modified_by", "description": "Modified"},
|
|
557
|
+
"modified_by": {"inverse": "modified", "description": "Modified by"},
|
|
558
|
+
"discovered": {"inverse": "discovered_by", "description": "Discovered"},
|
|
559
|
+
"discovered_by": {"inverse": "discovered", "description": "Discovered by"},
|
|
560
|
+
"reported": {"inverse": "reported_by", "description": "Reported"},
|
|
561
|
+
"reported_by": {"inverse": "reported", "description": "Reported by"},
|
|
562
|
+
|
|
563
|
+
# === LOCATION ===
|
|
564
|
+
"located_at": {"inverse": "location_of", "description": "Located at"},
|
|
565
|
+
"location_of": {"inverse": "located_at", "description": "Location of"},
|
|
566
|
+
"based_in": {"inverse": "base_of", "description": "Based in"},
|
|
567
|
+
"base_of": {"inverse": "based_in", "description": "Base of"},
|
|
568
|
+
"stored_in": {"inverse": "stores", "description": "Stored in"},
|
|
569
|
+
"stores": {"inverse": "stored_in", "description": "Stores"},
|
|
570
|
+
"accessible_from": {"inverse": "provides_access_to", "description": "Accessible from"},
|
|
571
|
+
"provides_access_to": {"inverse": "accessible_from", "description": "Provides access to"},
|
|
572
|
+
|
|
573
|
+
# === TIME/EVENTS ===
|
|
574
|
+
"scheduled": {"inverse": "has_event", "description": "Scheduled"},
|
|
575
|
+
"has_event": {"inverse": "scheduled", "description": "Has event"},
|
|
576
|
+
"attended": {"inverse": "attendee", "description": "Attended"},
|
|
577
|
+
"attendee": {"inverse": "attended", "description": "Attendee of"},
|
|
578
|
+
"occurred_in": {"inverse": "has_occurrence", "description": "Occurred in"},
|
|
579
|
+
"has_occurrence": {"inverse": "occurred_in", "description": "Has occurrence"},
|
|
580
|
+
"before": {"inverse": "after", "description": "Before"},
|
|
581
|
+
"after": {"inverse": "before", "description": "After"},
|
|
582
|
+
"during": {"inverse": "includes_event", "description": "During"},
|
|
583
|
+
"includes_event": {"inverse": "during", "description": "Includes event"},
|
|
584
|
+
|
|
585
|
+
# === DECISIONS ===
|
|
586
|
+
"decided_in": {"inverse": "has_decision", "description": "Decided in"},
|
|
587
|
+
"has_decision": {"inverse": "decided_in", "description": "Has decision"},
|
|
588
|
+
"participated_in": {"inverse": "participant", "description": "Participated in"},
|
|
589
|
+
"participant": {"inverse": "participated_in", "description": "Participant of"},
|
|
590
|
+
"approved_by": {"inverse": "approves", "description": "Approved by"},
|
|
591
|
+
"approves": {"inverse": "approved_by", "description": "Approves"},
|
|
592
|
+
"rejected_by": {"inverse": "rejects", "description": "Rejected by"},
|
|
593
|
+
"rejects": {"inverse": "rejected_by", "description": "Rejects"},
|
|
594
|
+
|
|
595
|
+
# === REFERENCES ===
|
|
596
|
+
"mentions": {"inverse": "mentioned_in", "description": "Mentions"},
|
|
597
|
+
"mentioned_in": {"inverse": "mentions", "description": "Mentioned in"},
|
|
598
|
+
"documents": {"inverse": "documented_in", "description": "Documents"},
|
|
599
|
+
"documented_in": {"inverse": "documents", "description": "Documented in"},
|
|
600
|
+
"proves": {"inverse": "proven_by", "description": "Proves"},
|
|
601
|
+
"proven_by": {"inverse": "proves", "description": "Proven by"},
|
|
602
|
+
"evidences": {"inverse": "evidenced_by", "description": "Evidences"},
|
|
603
|
+
"evidenced_by": {"inverse": "evidences", "description": "Evidenced by"},
|
|
604
|
+
|
|
605
|
+
# === COMMUNICATION ===
|
|
606
|
+
"contacted": {"inverse": "contacted_by", "description": "Contacted"},
|
|
607
|
+
"contacted_by": {"inverse": "contacted", "description": "Contacted by"},
|
|
608
|
+
"emailed": {"inverse": "emailed_by", "description": "Emailed"},
|
|
609
|
+
"emailed_by": {"inverse": "emailed", "description": "Emailed by"},
|
|
610
|
+
"messaged": {"inverse": "messaged_by", "description": "Messaged"},
|
|
611
|
+
"messaged_by": {"inverse": "messaged", "description": "Messaged by"},
|
|
612
|
+
"notified": {"inverse": "notified_by", "description": "Notified"},
|
|
613
|
+
"notified_by": {"inverse": "notified", "description": "Notified by"},
|
|
614
|
+
"asked": {"inverse": "asked_by", "description": "Asked"},
|
|
615
|
+
"asked_by": {"inverse": "asked", "description": "Asked by"},
|
|
616
|
+
"answered": {"inverse": "answered_by", "description": "Answered"},
|
|
617
|
+
"answered_by": {"inverse": "answered", "description": "Answered by"},
|
|
618
|
+
|
|
619
|
+
# === KNOWLEDGE ===
|
|
620
|
+
"learns": {"inverse": "learned_by", "description": "Learns"},
|
|
621
|
+
"learned_by": {"inverse": "learns", "description": "Learned by"},
|
|
622
|
+
"teaches": {"inverse": "taught_by", "description": "Teaches"},
|
|
623
|
+
"taught_by": {"inverse": "teaches", "description": "Taught by"},
|
|
624
|
+
"applies_to": {"inverse": "applied_in", "description": "Applies to"},
|
|
625
|
+
"applied_in": {"inverse": "applies_to", "description": "Applied in"},
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
class CortexDB:
|
|
630
|
+
"""Knowledge graph database."""
|
|
631
|
+
|
|
632
|
+
def __init__(self):
|
|
633
|
+
self.entities: Dict[str, dict] = {} # entity_name -> {type, attrs, created, modified}
|
|
634
|
+
self.relations: List[dict] = [] # [{from, relation, to, created, weight}]
|
|
635
|
+
self.events: List[dict] = [] # [{entity, event, timestamp, context}]
|
|
636
|
+
self._relation_index: Dict[str, List[int]] = defaultdict(list) # entity -> [relation_indices]
|
|
637
|
+
self.load()
|
|
638
|
+
|
|
639
|
+
def load(self):
|
|
640
|
+
"""Load data from files."""
|
|
641
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
642
|
+
|
|
643
|
+
if ENTITIES_FILE.exists():
|
|
644
|
+
try:
|
|
645
|
+
self.entities = json.loads(ENTITIES_FILE.read_text(encoding="utf-8"))
|
|
646
|
+
except:
|
|
647
|
+
self.entities = {}
|
|
648
|
+
|
|
649
|
+
if RELATIONS_FILE.exists():
|
|
650
|
+
try:
|
|
651
|
+
self.relations = json.loads(RELATIONS_FILE.read_text(encoding="utf-8"))
|
|
652
|
+
# Rebuild index
|
|
653
|
+
for i, r in enumerate(self.relations):
|
|
654
|
+
self._relation_index[self._norm(r["from"])].append(i)
|
|
655
|
+
self._relation_index[self._norm(r["to"])].append(i)
|
|
656
|
+
except:
|
|
657
|
+
self.relations = []
|
|
658
|
+
|
|
659
|
+
if EVENTS_FILE.exists():
|
|
660
|
+
try:
|
|
661
|
+
self.events = json.loads(EVENTS_FILE.read_text(encoding="utf-8"))
|
|
662
|
+
except:
|
|
663
|
+
self.events = []
|
|
664
|
+
|
|
665
|
+
def save(self):
|
|
666
|
+
"""Save data to files."""
|
|
667
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
668
|
+
ENTITIES_FILE.write_text(json.dumps(self.entities, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
669
|
+
RELATIONS_FILE.write_text(json.dumps(self.relations, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
670
|
+
EVENTS_FILE.write_text(json.dumps(self.events, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
671
|
+
|
|
672
|
+
def _norm(self, name: str) -> str:
|
|
673
|
+
"""Normalize entity name."""
|
|
674
|
+
return name.lower().strip()
|
|
675
|
+
|
|
676
|
+
def add_entity(self, name: str, entity_type: str, attrs: dict = None) -> bool:
|
|
677
|
+
"""Add or update an entity."""
|
|
678
|
+
key = self._norm(name)
|
|
679
|
+
now = datetime.now().isoformat()
|
|
680
|
+
|
|
681
|
+
if key in self.entities:
|
|
682
|
+
# Update existing
|
|
683
|
+
self.entities[key]["type"] = entity_type
|
|
684
|
+
if attrs:
|
|
685
|
+
self.entities[key]["attrs"].update(attrs)
|
|
686
|
+
self.entities[key]["modified"] = now
|
|
687
|
+
return False # Updated
|
|
688
|
+
|
|
689
|
+
# Create new
|
|
690
|
+
self.entities[key] = {
|
|
691
|
+
"name": name,
|
|
692
|
+
"type": entity_type,
|
|
693
|
+
"attrs": attrs or {},
|
|
694
|
+
"created": now,
|
|
695
|
+
"modified": now
|
|
696
|
+
}
|
|
697
|
+
return True # Created
|
|
698
|
+
|
|
699
|
+
def get_entity(self, name: str) -> Optional[dict]:
|
|
700
|
+
"""Get an entity."""
|
|
701
|
+
return self.entities.get(self._norm(name))
|
|
702
|
+
|
|
703
|
+
def delete_entity(self, name: str) -> bool:
|
|
704
|
+
"""Delete an entity and its relations."""
|
|
705
|
+
key = self._norm(name)
|
|
706
|
+
if key not in self.entities:
|
|
707
|
+
return False
|
|
708
|
+
|
|
709
|
+
del self.entities[key]
|
|
710
|
+
|
|
711
|
+
# Remove relations
|
|
712
|
+
indices_to_remove = self._relation_index.get(key, [])
|
|
713
|
+
for i in sorted(indices_to_remove, reverse=True):
|
|
714
|
+
if i < len(self.relations):
|
|
715
|
+
del self.relations[i]
|
|
716
|
+
|
|
717
|
+
# Rebuild index
|
|
718
|
+
self._relation_index = defaultdict(list)
|
|
719
|
+
for i, r in enumerate(self.relations):
|
|
720
|
+
self._relation_index[self._norm(r["from"])].append(i)
|
|
721
|
+
self._relation_index[self._norm(r["to"])].append(i)
|
|
722
|
+
|
|
723
|
+
self.save()
|
|
724
|
+
return True
|
|
725
|
+
|
|
726
|
+
def add_relation(self, from_entity: str, relation: str, to_entity: str, weight: float = 1.0) -> bool:
|
|
727
|
+
"""Add a relation between entities."""
|
|
728
|
+
from_key = self._norm(from_entity)
|
|
729
|
+
to_key = self._norm(to_entity)
|
|
730
|
+
relation = relation.lower()
|
|
731
|
+
|
|
732
|
+
if from_key not in self.entities:
|
|
733
|
+
return False
|
|
734
|
+
if to_key not in self.entities:
|
|
735
|
+
return False
|
|
736
|
+
|
|
737
|
+
# Check if relation already exists
|
|
738
|
+
for r in self.relations:
|
|
739
|
+
if (self._norm(r["from"]) == from_key and
|
|
740
|
+
r["relation"] == relation and
|
|
741
|
+
self._norm(r["to"]) == to_key):
|
|
742
|
+
return False # Already exists
|
|
743
|
+
|
|
744
|
+
# Add relation
|
|
745
|
+
rel_entry = {
|
|
746
|
+
"from": from_entity,
|
|
747
|
+
"relation": relation,
|
|
748
|
+
"to": to_entity,
|
|
749
|
+
"weight": weight,
|
|
750
|
+
"created": datetime.now().isoformat()
|
|
751
|
+
}
|
|
752
|
+
idx = len(self.relations)
|
|
753
|
+
self.relations.append(rel_entry)
|
|
754
|
+
self._relation_index[from_key].append(idx)
|
|
755
|
+
self._relation_index[to_key].append(idx)
|
|
756
|
+
|
|
757
|
+
# Add inverse relation if defined
|
|
758
|
+
if relation in RELATION_TYPES:
|
|
759
|
+
inverse = RELATION_TYPES[relation].get("inverse")
|
|
760
|
+
if inverse and inverse != relation:
|
|
761
|
+
inv_entry = {
|
|
762
|
+
"from": to_entity,
|
|
763
|
+
"relation": inverse,
|
|
764
|
+
"to": from_entity,
|
|
765
|
+
"weight": weight,
|
|
766
|
+
"created": datetime.now().isoformat(),
|
|
767
|
+
"inverse_of": True
|
|
768
|
+
}
|
|
769
|
+
idx2 = len(self.relations)
|
|
770
|
+
self.relations.append(inv_entry)
|
|
771
|
+
self._relation_index[to_key].append(idx2)
|
|
772
|
+
self._relation_index[from_key].append(idx2)
|
|
773
|
+
|
|
774
|
+
self.save()
|
|
775
|
+
return True
|
|
776
|
+
|
|
777
|
+
def get_relations(self, entity: str, direction: str = "both") -> List[dict]:
|
|
778
|
+
"""Get relations for an entity."""
|
|
779
|
+
key = self._norm(entity)
|
|
780
|
+
result = []
|
|
781
|
+
|
|
782
|
+
for i in self._relation_index.get(key, []):
|
|
783
|
+
if i >= len(self.relations):
|
|
784
|
+
continue
|
|
785
|
+
r = self.relations[i]
|
|
786
|
+
is_from = self._norm(r["from"]) == key
|
|
787
|
+
|
|
788
|
+
if direction == "out" and not is_from:
|
|
789
|
+
continue
|
|
790
|
+
if direction == "in" and is_from:
|
|
791
|
+
continue
|
|
792
|
+
|
|
793
|
+
result.append(r)
|
|
794
|
+
|
|
795
|
+
return result
|
|
796
|
+
|
|
797
|
+
def get_related_entities(self, entity: str, depth: int = 1) -> Set[str]:
|
|
798
|
+
"""Get related entities up to certain depth."""
|
|
799
|
+
key = self._norm(entity)
|
|
800
|
+
visited = {key}
|
|
801
|
+
frontier = {key}
|
|
802
|
+
|
|
803
|
+
for _ in range(depth):
|
|
804
|
+
next_frontier = set()
|
|
805
|
+
for e in frontier:
|
|
806
|
+
for r in self.get_relations(e):
|
|
807
|
+
other = self._norm(r["to"]) if self._norm(r["from"]) == e else self._norm(r["from"])
|
|
808
|
+
if other not in visited:
|
|
809
|
+
visited.add(other)
|
|
810
|
+
next_frontier.add(other)
|
|
811
|
+
frontier = next_frontier
|
|
812
|
+
if not frontier:
|
|
813
|
+
break
|
|
814
|
+
|
|
815
|
+
return visited - {key}
|
|
816
|
+
|
|
817
|
+
def find_path(self, from_entity: str, to_entity: str) -> Optional[List[Tuple[str, str, str]]]:
|
|
818
|
+
"""Find shortest path between two entities (BFS)."""
|
|
819
|
+
from_key = self._norm(from_entity)
|
|
820
|
+
to_key = self._norm(to_entity)
|
|
821
|
+
|
|
822
|
+
if from_key not in self.entities or to_key not in self.entities:
|
|
823
|
+
return None
|
|
824
|
+
|
|
825
|
+
if from_key == to_key:
|
|
826
|
+
return []
|
|
827
|
+
|
|
828
|
+
# BFS
|
|
829
|
+
queue = [(from_key, [])]
|
|
830
|
+
visited = {from_key}
|
|
831
|
+
|
|
832
|
+
while queue:
|
|
833
|
+
current, path = queue.pop(0)
|
|
834
|
+
|
|
835
|
+
for r in self.get_relations(current, "out"):
|
|
836
|
+
other = self._norm(r["to"])
|
|
837
|
+
new_path = path + [(r["from"], r["relation"], r["to"])]
|
|
838
|
+
|
|
839
|
+
if other == to_key:
|
|
840
|
+
return new_path
|
|
841
|
+
|
|
842
|
+
if other not in visited:
|
|
843
|
+
visited.add(other)
|
|
844
|
+
queue.append((other, new_path))
|
|
845
|
+
|
|
846
|
+
return None
|
|
847
|
+
|
|
848
|
+
def find_similar(self, entity: str) -> List[Tuple[str, float]]:
|
|
849
|
+
"""Find similar entities based on type and shared relations."""
|
|
850
|
+
key = self._norm(entity)
|
|
851
|
+
if key not in self.entities:
|
|
852
|
+
return []
|
|
853
|
+
|
|
854
|
+
entity_type = self.entities[key]["type"]
|
|
855
|
+
entity_relations = set()
|
|
856
|
+
|
|
857
|
+
for r in self.get_relations(key):
|
|
858
|
+
other = self._norm(r["to"]) if self._norm(r["from"]) == key else self._norm(r["from"])
|
|
859
|
+
entity_relations.add(other)
|
|
860
|
+
|
|
861
|
+
scores = []
|
|
862
|
+
for other_key, other_data in self.entities.items():
|
|
863
|
+
if other_key == key:
|
|
864
|
+
continue
|
|
865
|
+
|
|
866
|
+
score = 0.0
|
|
867
|
+
|
|
868
|
+
# Same type
|
|
869
|
+
if other_data["type"] == entity_type:
|
|
870
|
+
score += 0.5
|
|
871
|
+
|
|
872
|
+
# Shared relations
|
|
873
|
+
other_relations = set()
|
|
874
|
+
for r in self.get_relations(other_key):
|
|
875
|
+
third = self._norm(r["to"]) if self._norm(r["from"]) == other_key else self._norm(r["from"])
|
|
876
|
+
other_relations.add(third)
|
|
877
|
+
|
|
878
|
+
shared = entity_relations & other_relations
|
|
879
|
+
if entity_relations or other_relations:
|
|
880
|
+
jaccard = len(shared) / len(entity_relations | other_relations)
|
|
881
|
+
score += jaccard * 0.5
|
|
882
|
+
|
|
883
|
+
if score > 0:
|
|
884
|
+
scores.append((other_data["name"], score))
|
|
885
|
+
|
|
886
|
+
return sorted(scores, key=lambda x: -x[1])
|
|
887
|
+
|
|
888
|
+
def search_by_attrs(self, entity_type: str = None, attrs: dict = None) -> List[dict]:
|
|
889
|
+
"""Search entities by type and/or attributes."""
|
|
890
|
+
results = []
|
|
891
|
+
|
|
892
|
+
for key, data in self.entities.items():
|
|
893
|
+
if entity_type and data["type"] != entity_type:
|
|
894
|
+
continue
|
|
895
|
+
|
|
896
|
+
if attrs:
|
|
897
|
+
match = True
|
|
898
|
+
for k, v in attrs.items():
|
|
899
|
+
if data["attrs"].get(k) != v:
|
|
900
|
+
match = False
|
|
901
|
+
break
|
|
902
|
+
if not match:
|
|
903
|
+
continue
|
|
904
|
+
|
|
905
|
+
results.append(data)
|
|
906
|
+
|
|
907
|
+
return results
|
|
908
|
+
|
|
909
|
+
def add_event(self, entity: str, event: str, context: str = ""):
|
|
910
|
+
"""Add an episodic event."""
|
|
911
|
+
key = self._norm(entity)
|
|
912
|
+
if key not in self.entities:
|
|
913
|
+
return False
|
|
914
|
+
|
|
915
|
+
self.events.append({
|
|
916
|
+
"entity": entity,
|
|
917
|
+
"event": event,
|
|
918
|
+
"context": context,
|
|
919
|
+
"timestamp": datetime.now().isoformat()
|
|
920
|
+
})
|
|
921
|
+
self.save()
|
|
922
|
+
return True
|
|
923
|
+
|
|
924
|
+
def get_timeline(self, entity: str = None) -> List[dict]:
|
|
925
|
+
"""Get events ordered by time."""
|
|
926
|
+
if entity:
|
|
927
|
+
key = self._norm(entity)
|
|
928
|
+
events = [e for e in self.events if self._norm(e["entity"]) == key]
|
|
929
|
+
else:
|
|
930
|
+
events = self.events
|
|
931
|
+
|
|
932
|
+
return sorted(events, key=lambda x: x["timestamp"], reverse=True)
|
|
933
|
+
|
|
934
|
+
def get_stats(self) -> dict:
|
|
935
|
+
"""Get system statistics."""
|
|
936
|
+
type_counts = defaultdict(int)
|
|
937
|
+
for data in self.entities.values():
|
|
938
|
+
type_counts[data["type"]] += 1
|
|
939
|
+
|
|
940
|
+
rel_counts = defaultdict(int)
|
|
941
|
+
for r in self.relations:
|
|
942
|
+
rel_counts[r["relation"]] += 1
|
|
943
|
+
|
|
944
|
+
return {
|
|
945
|
+
"total_entities": len(self.entities),
|
|
946
|
+
"total_relations": len(self.relations),
|
|
947
|
+
"total_events": len(self.events),
|
|
948
|
+
"entity_types": dict(type_counts),
|
|
949
|
+
"relation_types": dict(rel_counts)
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
# Global instance
|
|
954
|
+
_db: Optional[CortexDB] = None
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def _get_db() -> CortexDB:
|
|
958
|
+
global _db
|
|
959
|
+
if _db is None:
|
|
960
|
+
_db = CortexDB()
|
|
961
|
+
return _db
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def run(args: str = "") -> str:
|
|
965
|
+
"""Plugin entry point."""
|
|
966
|
+
if not args.strip():
|
|
967
|
+
return _cmd_list("")
|
|
968
|
+
|
|
969
|
+
parts = args.strip().split(maxsplit=2)
|
|
970
|
+
cmd = parts[0].lower()
|
|
971
|
+
rest = parts[1] if len(parts) > 1 else ""
|
|
972
|
+
rest2 = parts[2] if len(parts) > 2 else ""
|
|
973
|
+
|
|
974
|
+
commands = {
|
|
975
|
+
"remember": _cmd_remember,
|
|
976
|
+
"memorize": _cmd_remember,
|
|
977
|
+
"learn": _cmd_remember,
|
|
978
|
+
"relate": _cmd_relate,
|
|
979
|
+
"link": _cmd_relate,
|
|
980
|
+
"connect": _cmd_relate,
|
|
981
|
+
"recall": _cmd_recall,
|
|
982
|
+
"get": _cmd_recall,
|
|
983
|
+
"show": _cmd_recall,
|
|
984
|
+
"find": _cmd_find,
|
|
985
|
+
"search": _cmd_find,
|
|
986
|
+
"query": _cmd_query,
|
|
987
|
+
"explore": _cmd_query,
|
|
988
|
+
"forget": _cmd_forget,
|
|
989
|
+
"delete": _cmd_forget,
|
|
990
|
+
"remove": _cmd_forget,
|
|
991
|
+
"list": _cmd_list,
|
|
992
|
+
"ls": _cmd_list,
|
|
993
|
+
"types": _cmd_types,
|
|
994
|
+
"relations": _cmd_relations,
|
|
995
|
+
"graph": _cmd_graph,
|
|
996
|
+
"visualize": _cmd_graph,
|
|
997
|
+
"timeline": _cmd_timeline,
|
|
998
|
+
"history": _cmd_timeline,
|
|
999
|
+
"similar": _cmd_similar,
|
|
1000
|
+
"related": _cmd_similar,
|
|
1001
|
+
"path": _cmd_path,
|
|
1002
|
+
"route": _cmd_path,
|
|
1003
|
+
"stats": _cmd_stats,
|
|
1004
|
+
"info": _cmd_stats,
|
|
1005
|
+
"demo": _cmd_demo,
|
|
1006
|
+
"init": _cmd_demo,
|
|
1007
|
+
"example": _cmd_demo,
|
|
1008
|
+
"reset": _cmd_reset,
|
|
1009
|
+
"clear": _cmd_reset,
|
|
1010
|
+
"wipe": _cmd_reset,
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
handler = commands.get(cmd)
|
|
1014
|
+
if not handler:
|
|
1015
|
+
return f"Unknown command: {cmd}\n\nCommands: remember, relate, recall, find, query, forget, list, types, relations, graph, timeline, similar, path, stats, demo, reset"
|
|
1016
|
+
|
|
1017
|
+
return handler(rest + (" " + rest2 if rest2 else ""))
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
def _parse_attrs(attr_str: str) -> dict:
|
|
1021
|
+
"""Parse attributes in key=value format."""
|
|
1022
|
+
attrs = {}
|
|
1023
|
+
# Match key=value or key="value with spaces"
|
|
1024
|
+
pattern = r'(\w+)=(?:"([^"]*)"|(\S+))'
|
|
1025
|
+
for match in re.finditer(pattern, attr_str):
|
|
1026
|
+
key = match.group(1)
|
|
1027
|
+
value = match.group(2) if match.group(2) is not None else match.group(3)
|
|
1028
|
+
attrs[key] = value
|
|
1029
|
+
return attrs
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
def _cmd_remember(args: str) -> str:
|
|
1033
|
+
"""Save an entity with attributes."""
|
|
1034
|
+
if not args.strip():
|
|
1035
|
+
return """Usage: cortex remember <name> <type> [key=value ...]
|
|
1036
|
+
|
|
1037
|
+
Types: person, project, target, host, vulnerability, codebase, module, function, api, bug, technology, event...
|
|
1038
|
+
|
|
1039
|
+
Examples:
|
|
1040
|
+
cortex remember John person role=developer team=backend
|
|
1041
|
+
cortex remember 'Auth API' project status=active language=python
|
|
1042
|
+
cortex remember target.example.com domain ip=1.2.3.4
|
|
1043
|
+
"""
|
|
1044
|
+
|
|
1045
|
+
parts = args.strip().split(maxsplit=2)
|
|
1046
|
+
if len(parts) < 2:
|
|
1047
|
+
return "Usage: cortex remember <name> <type> [key=value ...]"
|
|
1048
|
+
|
|
1049
|
+
name = parts[0].strip('"\'')
|
|
1050
|
+
entity_type = parts[1].lower()
|
|
1051
|
+
attr_str = parts[2] if len(parts) > 2 else ""
|
|
1052
|
+
|
|
1053
|
+
# Note: Type is NOT validated - any type can be used
|
|
1054
|
+
# Show warning only if type is not in suggested types
|
|
1055
|
+
type_warning = ""
|
|
1056
|
+
if entity_type not in ENTITY_TYPES:
|
|
1057
|
+
type_warning = f"\n (Note: '{entity_type}' is a custom type - that's fine!)"
|
|
1058
|
+
|
|
1059
|
+
attrs = _parse_attrs(attr_str)
|
|
1060
|
+
|
|
1061
|
+
db = _get_db()
|
|
1062
|
+
created = db.add_entity(name, entity_type, attrs)
|
|
1063
|
+
db.save()
|
|
1064
|
+
|
|
1065
|
+
action = "Created" if created else "Updated"
|
|
1066
|
+
type_info = ENTITY_TYPES.get(entity_type, {"description": "custom entity"})
|
|
1067
|
+
|
|
1068
|
+
lines = [
|
|
1069
|
+
f"ā {action} entity: {name}",
|
|
1070
|
+
f" Type: {entity_type} ({type_info['description']})"
|
|
1071
|
+
]
|
|
1072
|
+
|
|
1073
|
+
if attrs:
|
|
1074
|
+
lines.append(" Attributes:")
|
|
1075
|
+
for k, v in attrs.items():
|
|
1076
|
+
lines.append(f" {k} = {v}")
|
|
1077
|
+
|
|
1078
|
+
if type_warning:
|
|
1079
|
+
lines.append(type_warning)
|
|
1080
|
+
|
|
1081
|
+
return "\n".join(lines)
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def _cmd_relate(args: str) -> str:
|
|
1085
|
+
"""Create a relation between entities."""
|
|
1086
|
+
if not args.strip():
|
|
1087
|
+
# Show available relations
|
|
1088
|
+
lines = ["Usage: cortex relate <entity1> <relation> <entity2>", "", "Common relations:"]
|
|
1089
|
+
shown = set()
|
|
1090
|
+
for rel, info in list(RELATION_TYPES.items())[:20]:
|
|
1091
|
+
if rel in shown or info.get("inverse") in shown:
|
|
1092
|
+
continue
|
|
1093
|
+
shown.add(rel)
|
|
1094
|
+
inv = f" ā {info['inverse']}" if info.get("inverse") and info["inverse"] != rel else ""
|
|
1095
|
+
lines.append(f" {rel}: {info['description']}{inv}")
|
|
1096
|
+
lines.append("\n (Use any relation - these are just suggestions)")
|
|
1097
|
+
return "\n".join(lines)
|
|
1098
|
+
|
|
1099
|
+
parts = args.strip().split(maxsplit=2)
|
|
1100
|
+
if len(parts) < 3:
|
|
1101
|
+
return "Usage: cortex relate <entity1> <relation> <entity2>"
|
|
1102
|
+
|
|
1103
|
+
from_entity = parts[0].strip('"\'')
|
|
1104
|
+
relation = parts[1].lower()
|
|
1105
|
+
to_entity = parts[2].strip('"\'')
|
|
1106
|
+
|
|
1107
|
+
db = _get_db()
|
|
1108
|
+
|
|
1109
|
+
# Check entities exist
|
|
1110
|
+
if not db.get_entity(from_entity):
|
|
1111
|
+
return f"Entity not found: {from_entity}"
|
|
1112
|
+
if not db.get_entity(to_entity):
|
|
1113
|
+
return f"Entity not found: {to_entity}"
|
|
1114
|
+
|
|
1115
|
+
# Note: Relation is NOT validated - any relation can be used
|
|
1116
|
+
# Show warning only if relation is not in suggested types
|
|
1117
|
+
rel_warning = ""
|
|
1118
|
+
if relation not in RELATION_TYPES:
|
|
1119
|
+
rel_warning = f"\n (Note: '{relation}' is a custom relation - that's fine!)"
|
|
1120
|
+
|
|
1121
|
+
if db.add_relation(from_entity, relation, to_entity):
|
|
1122
|
+
inv_info = ""
|
|
1123
|
+
if relation in RELATION_TYPES:
|
|
1124
|
+
inv = RELATION_TYPES[relation].get("inverse")
|
|
1125
|
+
if inv and inv != relation:
|
|
1126
|
+
inv_info = f"\n (Auto-created inverse: {to_entity} {inv} {from_entity})"
|
|
1127
|
+
|
|
1128
|
+
return f"ā Relation created:\n {from_entity} ā {relation} ā {to_entity}{inv_info}{rel_warning}"
|
|
1129
|
+
else:
|
|
1130
|
+
return f"Relation already exists: {from_entity} ā {relation} ā {to_entity}"
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
def _cmd_recall(args: str) -> str:
|
|
1134
|
+
"""Get information about an entity."""
|
|
1135
|
+
if not args.strip():
|
|
1136
|
+
return "Usage: cortex recall <entity>"
|
|
1137
|
+
|
|
1138
|
+
name = args.strip().strip('"\'')
|
|
1139
|
+
db = _get_db()
|
|
1140
|
+
entity = db.get_entity(name)
|
|
1141
|
+
|
|
1142
|
+
if not entity:
|
|
1143
|
+
# Try fuzzy match
|
|
1144
|
+
matches = [e for e in db.entities if name.lower() in e]
|
|
1145
|
+
if matches:
|
|
1146
|
+
return f"Entity not found.\nSimilar: {', '.join(db.entities[m]['name'] for m in matches[:5])}"
|
|
1147
|
+
return f"Entity not found: {name}"
|
|
1148
|
+
|
|
1149
|
+
type_info = ENTITY_TYPES.get(entity["type"], {"description": "custom entity"})
|
|
1150
|
+
|
|
1151
|
+
lines = [
|
|
1152
|
+
f"āāā {entity['name']} āāā",
|
|
1153
|
+
f"Type: {entity['type']} ({type_info.get('description', 'unknown')})"
|
|
1154
|
+
]
|
|
1155
|
+
|
|
1156
|
+
if entity["attrs"]:
|
|
1157
|
+
lines.append("\nAttributes:")
|
|
1158
|
+
for k, v in entity["attrs"].items():
|
|
1159
|
+
lines.append(f" {k}: {v}")
|
|
1160
|
+
|
|
1161
|
+
# Outgoing relations
|
|
1162
|
+
out_rels = db.get_relations(name, "out")
|
|
1163
|
+
if out_rels:
|
|
1164
|
+
lines.append("\nOutgoing relations:")
|
|
1165
|
+
for r in out_rels[:10]:
|
|
1166
|
+
rel_info = RELATION_TYPES.get(r["relation"], {})
|
|
1167
|
+
desc = rel_info.get("description", r["relation"])
|
|
1168
|
+
lines.append(f" ā {desc} ā {r['to']}")
|
|
1169
|
+
|
|
1170
|
+
# Incoming relations
|
|
1171
|
+
in_rels = db.get_relations(name, "in")
|
|
1172
|
+
if in_rels:
|
|
1173
|
+
lines.append("\nIncoming relations:")
|
|
1174
|
+
for r in in_rels[:10]:
|
|
1175
|
+
rel_info = RELATION_TYPES.get(r["relation"], {})
|
|
1176
|
+
desc = rel_info.get("description", r["relation"])
|
|
1177
|
+
lines.append(f" ā {desc} ā {r['from']}")
|
|
1178
|
+
|
|
1179
|
+
# Recent events
|
|
1180
|
+
events = [e for e in db.events if db._norm(e["entity"]) == db._norm(name)]
|
|
1181
|
+
if events:
|
|
1182
|
+
lines.append(f"\nEvents ({len(events)}):")
|
|
1183
|
+
for e in events[-5:]:
|
|
1184
|
+
ts = e["timestamp"][:10]
|
|
1185
|
+
lines.append(f" [{ts}] {e['event']}")
|
|
1186
|
+
|
|
1187
|
+
lines.append(f"\nCreated: {entity['created'][:10]}")
|
|
1188
|
+
lines.append(f"Modified: {entity['modified'][:10]}")
|
|
1189
|
+
|
|
1190
|
+
return "\n".join(lines)
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
def _cmd_find(args: str) -> str:
|
|
1194
|
+
"""Search entities by type and/or attributes."""
|
|
1195
|
+
if not args.strip():
|
|
1196
|
+
db = _get_db()
|
|
1197
|
+
stats = db.get_stats()
|
|
1198
|
+
lines = ["Usage: cortex find <type> [key=value ...]", "", "Entities by type:"]
|
|
1199
|
+
for t, count in sorted(stats["entity_types"].items()):
|
|
1200
|
+
type_info = ENTITY_TYPES.get(t, {})
|
|
1201
|
+
desc = type_info.get("description", "custom")
|
|
1202
|
+
lines.append(f" {t}: {count} ({desc})")
|
|
1203
|
+
return "\n".join(lines)
|
|
1204
|
+
|
|
1205
|
+
parts = args.strip().split(maxsplit=1)
|
|
1206
|
+
entity_type = parts[0].lower()
|
|
1207
|
+
attrs = _parse_attrs(parts[1]) if len(parts) > 1 else {}
|
|
1208
|
+
|
|
1209
|
+
db = _get_db()
|
|
1210
|
+
|
|
1211
|
+
# If type is not valid, search in all types
|
|
1212
|
+
if entity_type not in ENTITY_TYPES:
|
|
1213
|
+
# Treat as attribute search
|
|
1214
|
+
attrs = _parse_attrs(args)
|
|
1215
|
+
entity_type = None
|
|
1216
|
+
|
|
1217
|
+
results = db.search_by_attrs(entity_type, attrs)
|
|
1218
|
+
|
|
1219
|
+
if not results:
|
|
1220
|
+
return "No entities found."
|
|
1221
|
+
|
|
1222
|
+
lines = [f"Found {len(results)} entities:"]
|
|
1223
|
+
for e in results[:20]:
|
|
1224
|
+
attrs_str = ", ".join(f"{k}={v}" for k, v in list(e["attrs"].items())[:3])
|
|
1225
|
+
if attrs_str:
|
|
1226
|
+
attrs_str = f" ({attrs_str})"
|
|
1227
|
+
lines.append(f" ⢠[{e['type']}] {e['name']}{attrs_str}")
|
|
1228
|
+
|
|
1229
|
+
return "\n".join(lines)
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
def _cmd_query(args: str) -> str:
|
|
1233
|
+
"""Graph query with depth."""
|
|
1234
|
+
if not args.strip():
|
|
1235
|
+
return "Usage: cortex query <entity> [depth]"
|
|
1236
|
+
|
|
1237
|
+
parts = args.strip().split()
|
|
1238
|
+
name = parts[0].strip('"\'')
|
|
1239
|
+
depth = int(parts[1]) if len(parts) > 1 else 2
|
|
1240
|
+
|
|
1241
|
+
db = _get_db()
|
|
1242
|
+
entity = db.get_entity(name)
|
|
1243
|
+
|
|
1244
|
+
if not entity:
|
|
1245
|
+
return f"Entity not found: {name}"
|
|
1246
|
+
|
|
1247
|
+
related = db.get_related_entities(name, depth)
|
|
1248
|
+
|
|
1249
|
+
if not related:
|
|
1250
|
+
return f"No entities related to '{name}'"
|
|
1251
|
+
|
|
1252
|
+
lines = [f"Entities related to '{name}' (depth {depth}):"]
|
|
1253
|
+
lines.append("=" * 50)
|
|
1254
|
+
|
|
1255
|
+
# Group by distance
|
|
1256
|
+
visited = {db._norm(name)}
|
|
1257
|
+
current_level = {db._norm(name)}
|
|
1258
|
+
|
|
1259
|
+
for d in range(1, depth + 1):
|
|
1260
|
+
next_level = set()
|
|
1261
|
+
for e in current_level:
|
|
1262
|
+
for r in db.get_relations(e, "out"):
|
|
1263
|
+
other = db._norm(r["to"])
|
|
1264
|
+
if other not in visited:
|
|
1265
|
+
visited.add(other)
|
|
1266
|
+
next_level.add(other)
|
|
1267
|
+
e_name = db.entities.get(e, {}).get("name", e)
|
|
1268
|
+
other_name = db.entities.get(other, {}).get("name", other)
|
|
1269
|
+
rel_desc = RELATION_TYPES.get(r["relation"], {}).get("description", r["relation"])
|
|
1270
|
+
lines.append(f" {' ' * (d-1)}{e_name} ā {rel_desc} ā {other_name}")
|
|
1271
|
+
|
|
1272
|
+
current_level = next_level
|
|
1273
|
+
if not current_level:
|
|
1274
|
+
break
|
|
1275
|
+
|
|
1276
|
+
lines.append(f"\nTotal: {len(related)} related entities")
|
|
1277
|
+
|
|
1278
|
+
return "\n".join(lines)
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
def _cmd_forget(args: str) -> str:
|
|
1282
|
+
"""Delete an entity."""
|
|
1283
|
+
if not args.strip():
|
|
1284
|
+
return "Usage: cortex forget <entity>"
|
|
1285
|
+
|
|
1286
|
+
name = args.strip().strip('"\'')
|
|
1287
|
+
db = _get_db()
|
|
1288
|
+
|
|
1289
|
+
entity = db.get_entity(name)
|
|
1290
|
+
if not entity:
|
|
1291
|
+
return f"Entity not found: {name}"
|
|
1292
|
+
|
|
1293
|
+
# Count relations
|
|
1294
|
+
rels = db.get_relations(name)
|
|
1295
|
+
|
|
1296
|
+
db.delete_entity(name)
|
|
1297
|
+
|
|
1298
|
+
return f"ā Deleted entity: {name}\n {len(rels)} relations removed"
|
|
1299
|
+
|
|
1300
|
+
|
|
1301
|
+
def _cmd_list(args: str) -> str:
|
|
1302
|
+
"""List all entities."""
|
|
1303
|
+
db = _get_db()
|
|
1304
|
+
stats = db.get_stats()
|
|
1305
|
+
|
|
1306
|
+
if not db.entities:
|
|
1307
|
+
return "No entities saved.\n\nUse: cortex remember <name> <type> [attrs]"
|
|
1308
|
+
|
|
1309
|
+
lines = [f"Entities ({len(db.entities)}):"]
|
|
1310
|
+
lines.append("=" * 50)
|
|
1311
|
+
|
|
1312
|
+
# Group by type
|
|
1313
|
+
by_type = defaultdict(list)
|
|
1314
|
+
for key, data in db.entities.items():
|
|
1315
|
+
by_type[data["type"]].append(data)
|
|
1316
|
+
|
|
1317
|
+
for etype in sorted(by_type.keys()):
|
|
1318
|
+
type_info = ENTITY_TYPES.get(etype, {})
|
|
1319
|
+
lines.append(f"\n[{etype}] {type_info.get('description', 'custom')}:")
|
|
1320
|
+
|
|
1321
|
+
for e in sorted(by_type[etype], key=lambda x: x["name"]):
|
|
1322
|
+
rels = db.get_relations(e["name"])
|
|
1323
|
+
attrs_str = ""
|
|
1324
|
+
if e["attrs"]:
|
|
1325
|
+
attrs_str = " ā " + ", ".join(f"{k}={v}" for k, v in list(e["attrs"].items())[:2])
|
|
1326
|
+
lines.append(f" ⢠{e['name']}{attrs_str} ({len(rels)} rels)")
|
|
1327
|
+
|
|
1328
|
+
lines.append(f"\n" + "=" * 50)
|
|
1329
|
+
lines.append(f"Total relations: {len(db.relations)}")
|
|
1330
|
+
lines.append(f"Total events: {len(db.events)}")
|
|
1331
|
+
|
|
1332
|
+
return "\n".join(lines)
|
|
1333
|
+
|
|
1334
|
+
|
|
1335
|
+
def _cmd_types(args: str) -> str:
|
|
1336
|
+
"""Show entity types."""
|
|
1337
|
+
lines = ["Entity types (suggestions - you can use any type):", "=" * 50]
|
|
1338
|
+
|
|
1339
|
+
for etype, info in sorted(ENTITY_TYPES.items()):
|
|
1340
|
+
attrs = ", ".join(info["default_attrs"][:5])
|
|
1341
|
+
lines.append(f"\n{etype}:")
|
|
1342
|
+
lines.append(f" {info['description']}")
|
|
1343
|
+
if attrs:
|
|
1344
|
+
lines.append(f" Common attrs: {attrs}")
|
|
1345
|
+
|
|
1346
|
+
return "\n".join(lines)
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
def _cmd_relations(args: str) -> str:
|
|
1350
|
+
"""Show relation types."""
|
|
1351
|
+
lines = ["Relation types (suggestions - you can use any relation):", "=" * 50]
|
|
1352
|
+
|
|
1353
|
+
# Group by category
|
|
1354
|
+
categories = {
|
|
1355
|
+
"Social": ["knows", "works_with", "reports_to", "manages", "collaborates_with"],
|
|
1356
|
+
"Projects": ["works_on", "has_member", "leads", "led_by", "owns", "owned_by", "maintains"],
|
|
1357
|
+
"Code": ["imports", "calls", "extends", "implements", "contains", "defines", "references"],
|
|
1358
|
+
"Dependencies": ["depends_on", "requires", "uses", "includes"],
|
|
1359
|
+
"Development": ["fixes", "introduces", "causes", "affects", "tests", "covers", "merges"],
|
|
1360
|
+
"Security - Infrastructure": ["hosts", "runs_on", "has_port", "has_service", "resolves_to"],
|
|
1361
|
+
"Security - Attacks": ["has_vuln", "exploits", "targets", "attacks", "scans", "finds", "detects", "bypasses", "mitigates"],
|
|
1362
|
+
"Security - Credentials": ["leaks", "uses_credential", "valid_for", "compromises"],
|
|
1363
|
+
"Security - Tools": ["runs_tool", "generates", "produces", "consumes"],
|
|
1364
|
+
"Structural": ["related_to", "part_of", "parent_of", "child_of", "similar_to", "same_as"],
|
|
1365
|
+
"Creation": ["created", "created_by", "modified", "modified_by", "discovered", "reported"],
|
|
1366
|
+
"Location": ["located_at", "based_in", "stored_in", "accessible_from"],
|
|
1367
|
+
"Events": ["scheduled", "attended", "occurred_in", "before", "after", "during"],
|
|
1368
|
+
"Decisions": ["decided_in", "participated_in", "approved_by", "rejected_by"],
|
|
1369
|
+
"References": ["mentions", "documents", "proves", "evidences"],
|
|
1370
|
+
"Communication": ["contacted", "emailed", "messaged", "notified", "asked", "answered"],
|
|
1371
|
+
"Knowledge": ["learns", "teaches", "applies_to"],
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
for cat, rels in categories.items():
|
|
1375
|
+
cat_lines = []
|
|
1376
|
+
for rel in rels:
|
|
1377
|
+
if rel in RELATION_TYPES:
|
|
1378
|
+
info = RELATION_TYPES[rel]
|
|
1379
|
+
inv = f" ā {info['inverse']}" if info.get("inverse") and info["inverse"] != rel else ""
|
|
1380
|
+
cat_lines.append(f" {rel}: {info['description']}{inv}")
|
|
1381
|
+
if cat_lines:
|
|
1382
|
+
lines.append(f"\n{cat}:")
|
|
1383
|
+
lines.extend(cat_lines)
|
|
1384
|
+
|
|
1385
|
+
return "\n".join(lines)
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
def _cmd_graph(args: str) -> str:
|
|
1389
|
+
"""Visualize the graph."""
|
|
1390
|
+
db = _get_db()
|
|
1391
|
+
|
|
1392
|
+
if not db.entities:
|
|
1393
|
+
return "No entities to visualize."
|
|
1394
|
+
|
|
1395
|
+
if args.strip():
|
|
1396
|
+
# Graph centered on entity
|
|
1397
|
+
name = args.strip().strip('"\'')
|
|
1398
|
+
entity = db.get_entity(name)
|
|
1399
|
+
if not entity:
|
|
1400
|
+
return f"Entity not found: {name}"
|
|
1401
|
+
|
|
1402
|
+
related = db.get_related_entities(name, 2)
|
|
1403
|
+
entities_to_show = {db._norm(name)} | related
|
|
1404
|
+
else:
|
|
1405
|
+
entities_to_show = set(db.entities.keys())
|
|
1406
|
+
|
|
1407
|
+
lines = ["Knowledge graph:", "=" * 50]
|
|
1408
|
+
|
|
1409
|
+
# Build adjacency for display
|
|
1410
|
+
for key in sorted(entities_to_show):
|
|
1411
|
+
if key not in db.entities:
|
|
1412
|
+
continue
|
|
1413
|
+
|
|
1414
|
+
data = db.entities[key]
|
|
1415
|
+
type_emoji = {
|
|
1416
|
+
"person": "š¤",
|
|
1417
|
+
"project": "š",
|
|
1418
|
+
"concept": "š”",
|
|
1419
|
+
"event": "š
",
|
|
1420
|
+
"preference": "ā",
|
|
1421
|
+
"organization": "š¢",
|
|
1422
|
+
"technology": "āļø",
|
|
1423
|
+
"location": "š",
|
|
1424
|
+
"task": "ā
",
|
|
1425
|
+
"decision": "šÆ",
|
|
1426
|
+
"target": "šÆ",
|
|
1427
|
+
"host": "š„ļø",
|
|
1428
|
+
"domain": "š",
|
|
1429
|
+
"vulnerability": "š“",
|
|
1430
|
+
"exploit": "š„",
|
|
1431
|
+
"credential": "š",
|
|
1432
|
+
"tool": "š§",
|
|
1433
|
+
"codebase": "š¦",
|
|
1434
|
+
"module": "š¦",
|
|
1435
|
+
"api": "š",
|
|
1436
|
+
}.get(data["type"], "š")
|
|
1437
|
+
|
|
1438
|
+
lines.append(f"\n{type_emoji} {data['name']} [{data['type']}]")
|
|
1439
|
+
|
|
1440
|
+
# Show outgoing relations
|
|
1441
|
+
for r in db.get_relations(key, "out"):
|
|
1442
|
+
other_key = db._norm(r["to"])
|
|
1443
|
+
if other_key in entities_to_show:
|
|
1444
|
+
rel_desc = RELATION_TYPES.get(r["relation"], {}).get("description", r["relation"])
|
|
1445
|
+
other_name = db.entities.get(other_key, {}).get("name", other_key)
|
|
1446
|
+
lines.append(f" ā {rel_desc} ā {other_name}")
|
|
1447
|
+
|
|
1448
|
+
lines.append("\n" + "=" * 50)
|
|
1449
|
+
lines.append(f"Nodes: {len(entities_to_show)}")
|
|
1450
|
+
|
|
1451
|
+
return "\n".join(lines)
|
|
1452
|
+
|
|
1453
|
+
|
|
1454
|
+
def _cmd_timeline(args: str) -> str:
|
|
1455
|
+
"""Show events in temporal order."""
|
|
1456
|
+
db = _get_db()
|
|
1457
|
+
|
|
1458
|
+
entity = args.strip().strip('"\'') if args.strip() else None
|
|
1459
|
+
|
|
1460
|
+
events = db.get_timeline(entity)
|
|
1461
|
+
|
|
1462
|
+
if not events:
|
|
1463
|
+
if entity:
|
|
1464
|
+
return f"No events for: {entity}"
|
|
1465
|
+
return "No events recorded."
|
|
1466
|
+
|
|
1467
|
+
lines = [f"Timeline{' for ' + entity if entity else ''}:", "=" * 50]
|
|
1468
|
+
|
|
1469
|
+
for e in events[:30]:
|
|
1470
|
+
ts = e["timestamp"][:16]
|
|
1471
|
+
entity_name = db.entities.get(db._norm(e["entity"]), {}).get("name", e["entity"])
|
|
1472
|
+
lines.append(f" [{ts}] {entity_name}: {e['event']}")
|
|
1473
|
+
if e.get("context"):
|
|
1474
|
+
lines.append(f" {e['context'][:50]}")
|
|
1475
|
+
|
|
1476
|
+
if len(events) > 30:
|
|
1477
|
+
lines.append(f"\n ... and {len(events) - 30} more events")
|
|
1478
|
+
|
|
1479
|
+
return "\n".join(lines)
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
def _cmd_similar(args: str) -> str:
|
|
1483
|
+
"""Find similar entities."""
|
|
1484
|
+
if not args.strip():
|
|
1485
|
+
return "Usage: cortex similar <entity>"
|
|
1486
|
+
|
|
1487
|
+
name = args.strip().strip('"\'')
|
|
1488
|
+
db = _get_db()
|
|
1489
|
+
|
|
1490
|
+
entity = db.get_entity(name)
|
|
1491
|
+
if not entity:
|
|
1492
|
+
return f"Entity not found: {name}"
|
|
1493
|
+
|
|
1494
|
+
similar = db.find_similar(name)
|
|
1495
|
+
|
|
1496
|
+
if not similar:
|
|
1497
|
+
return f"No entities similar to '{name}'"
|
|
1498
|
+
|
|
1499
|
+
lines = [f"Entities similar to '{name}':", "=" * 50]
|
|
1500
|
+
|
|
1501
|
+
for other_name, score in similar[:10]:
|
|
1502
|
+
other = db.get_entity(other_name)
|
|
1503
|
+
if other:
|
|
1504
|
+
lines.append(f" ⢠{other_name} [{other['type']}] - {score:.0%} similar")
|
|
1505
|
+
# Show why
|
|
1506
|
+
shared_rels = set()
|
|
1507
|
+
for r in db.get_relations(name):
|
|
1508
|
+
other_rel = db._norm(r["to"]) if db._norm(r["from"]) == db._norm(name) else db._norm(r["from"])
|
|
1509
|
+
shared_rels.add(other_rel)
|
|
1510
|
+
for r in db.get_relations(other_name):
|
|
1511
|
+
third = db._norm(r["to"]) if db._norm(r["from"]) == db._norm(other_name) else db._norm(r["from"])
|
|
1512
|
+
if third in shared_rels:
|
|
1513
|
+
lines.append(f" Common connection: {db.entities.get(third, {}).get('name', third)}")
|
|
1514
|
+
|
|
1515
|
+
return "\n".join(lines)
|
|
1516
|
+
|
|
1517
|
+
|
|
1518
|
+
def _cmd_path(args: str) -> str:
|
|
1519
|
+
"""Find path between entities."""
|
|
1520
|
+
if not args.strip():
|
|
1521
|
+
return "Usage: cortex path <entity1> <entity2>"
|
|
1522
|
+
|
|
1523
|
+
parts = args.strip().split()
|
|
1524
|
+
if len(parts) < 2:
|
|
1525
|
+
return "Usage: cortex path <entity1> <entity2>"
|
|
1526
|
+
|
|
1527
|
+
from_name = parts[0].strip('"\'')
|
|
1528
|
+
to_name = parts[1].strip('"\'')
|
|
1529
|
+
|
|
1530
|
+
db = _get_db()
|
|
1531
|
+
|
|
1532
|
+
path = db.find_path(from_name, to_name)
|
|
1533
|
+
|
|
1534
|
+
if path is None:
|
|
1535
|
+
return f"No path between '{from_name}' and '{to_name}'"
|
|
1536
|
+
|
|
1537
|
+
if not path:
|
|
1538
|
+
return f"Same entity: {from_name}"
|
|
1539
|
+
|
|
1540
|
+
lines = [f"Path from '{from_name}' to '{to_name}':", "=" * 50]
|
|
1541
|
+
|
|
1542
|
+
for from_e, rel, to_e in path:
|
|
1543
|
+
rel_desc = RELATION_TYPES.get(rel, {}).get("description", rel)
|
|
1544
|
+
lines.append(f" {from_e} ā {rel_desc} ā {to_e}")
|
|
1545
|
+
|
|
1546
|
+
lines.append(f"\nDistance: {len(path)} steps")
|
|
1547
|
+
|
|
1548
|
+
return "\n".join(lines)
|
|
1549
|
+
|
|
1550
|
+
|
|
1551
|
+
def _cmd_stats(args: str) -> str:
|
|
1552
|
+
"""Show statistics."""
|
|
1553
|
+
db = _get_db()
|
|
1554
|
+
stats = db.get_stats()
|
|
1555
|
+
|
|
1556
|
+
lines = [
|
|
1557
|
+
"āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā",
|
|
1558
|
+
" CORTEX - STATISTICS",
|
|
1559
|
+
"āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā",
|
|
1560
|
+
"",
|
|
1561
|
+
f"Total entities: {stats['total_entities']}",
|
|
1562
|
+
f"Total relations: {stats['total_relations']}",
|
|
1563
|
+
f"Total events: {stats['total_events']}",
|
|
1564
|
+
"",
|
|
1565
|
+
"Entities by type:",
|
|
1566
|
+
]
|
|
1567
|
+
|
|
1568
|
+
for t, count in sorted(stats["entity_types"].items(), key=lambda x: -x[1]):
|
|
1569
|
+
type_info = ENTITY_TYPES.get(t, {})
|
|
1570
|
+
desc = type_info.get("description", "custom")
|
|
1571
|
+
bar = "ā" * min(count, 20)
|
|
1572
|
+
lines.append(f" {t:<12} {bar} {count}")
|
|
1573
|
+
|
|
1574
|
+
lines.append("\nMost used relations:")
|
|
1575
|
+
|
|
1576
|
+
for r, count in sorted(stats["relation_types"].items(), key=lambda x: -x[1])[:10]:
|
|
1577
|
+
rel_info = RELATION_TYPES.get(r, {})
|
|
1578
|
+
desc = rel_info.get("description", r)
|
|
1579
|
+
lines.append(f" {r:<15} {count:>3} ({desc})")
|
|
1580
|
+
|
|
1581
|
+
return "\n".join(lines)
|
|
1582
|
+
|
|
1583
|
+
|
|
1584
|
+
def _cmd_demo(args: str) -> str:
|
|
1585
|
+
"""Generate demo data for testing."""
|
|
1586
|
+
db = _get_db()
|
|
1587
|
+
|
|
1588
|
+
# Check if already has data
|
|
1589
|
+
if db.entities:
|
|
1590
|
+
return f"Database already has {len(db.entities)} entities.\nUse '/cortex forget <name>' to remove or '/cortex list' to see them.\n\nTo reset and add demo data: /cortex demo --force"
|
|
1591
|
+
|
|
1592
|
+
demo_type = args.strip().lower() if args.strip() else "all"
|
|
1593
|
+
|
|
1594
|
+
lines = ["Generating demo data...", "=" * 50]
|
|
1595
|
+
|
|
1596
|
+
# === PROGRAMMING DEMO DATA ===
|
|
1597
|
+
if demo_type in ["all", "programming", "code"]:
|
|
1598
|
+
lines.append("\nš Programming Demo:")
|
|
1599
|
+
|
|
1600
|
+
# People
|
|
1601
|
+
db.add_entity("Alice", "person", {"role": "lead developer", "team": "backend", "email": "alice@example.com"})
|
|
1602
|
+
db.add_entity("Bob", "person", {"role": "developer", "team": "backend", "email": "bob@example.com"})
|
|
1603
|
+
db.add_entity("Carol", "person", {"role": "devops", "team": "infrastructure", "email": "carol@example.com"})
|
|
1604
|
+
|
|
1605
|
+
# Projects
|
|
1606
|
+
db.add_entity("HanusCode", "project", {"status": "active", "language": "python", "priority": "high"})
|
|
1607
|
+
db.add_entity("WebUI", "project", {"status": "active", "language": "javascript", "priority": "medium"})
|
|
1608
|
+
|
|
1609
|
+
# Codebase
|
|
1610
|
+
db.add_entity("hanus-core", "codebase", {"language": "python", "framework": "none", "size": "15k LOC"})
|
|
1611
|
+
db.add_entity("hanus-plugins", "module", {"path": "hanus/plugins/", "purpose": "plugin system"})
|
|
1612
|
+
db.add_entity("cortex", "module", {"path": "hanus/plugins/cortex.py", "purpose": "semantic memory"})
|
|
1613
|
+
|
|
1614
|
+
# Technologies
|
|
1615
|
+
db.add_entity("Python", "technology", {"type": "language", "version": "3.11"})
|
|
1616
|
+
db.add_entity("FastAPI", "technology", {"type": "framework", "category": "web"})
|
|
1617
|
+
db.add_entity("SQLite", "technology", {"type": "database", "category": "storage"})
|
|
1618
|
+
|
|
1619
|
+
# Relations
|
|
1620
|
+
db.add_relation("Alice", "leads", "HanusCode")
|
|
1621
|
+
db.add_relation("Alice", "works_on", "hanus-core")
|
|
1622
|
+
db.add_relation("Bob", "works_on", "hanus-plugins")
|
|
1623
|
+
db.add_relation("Bob", "works_on", "WebUI")
|
|
1624
|
+
db.add_relation("Carol", "maintains", "HanusCode")
|
|
1625
|
+
db.add_relation("HanusCode", "uses", "Python")
|
|
1626
|
+
db.add_relation("HanusCode", "uses", "FastAPI")
|
|
1627
|
+
db.add_relation("HanusCode", "uses", "SQLite")
|
|
1628
|
+
db.add_relation("hanus-core", "contains", "hanus-plugins")
|
|
1629
|
+
db.add_relation("hanus-plugins", "contains", "cortex")
|
|
1630
|
+
db.add_relation("Alice", "knows", "Bob")
|
|
1631
|
+
db.add_relation("Bob", "knows", "Carol")
|
|
1632
|
+
|
|
1633
|
+
lines.append(" ā Added programming entities (people, projects, code)")
|
|
1634
|
+
|
|
1635
|
+
# === SECURITY DEMO DATA ===
|
|
1636
|
+
if demo_type in ["all", "security", "pentest"]:
|
|
1637
|
+
lines.append("\nš Security Demo:")
|
|
1638
|
+
|
|
1639
|
+
# Targets
|
|
1640
|
+
db.add_entity("target.local", "target", {"type": "network", "scope": "internal", "status": "in_scope"})
|
|
1641
|
+
db.add_entity("web.target.local", "domain", {"ip": "192.168.1.100", "ports": "80,443,8080"})
|
|
1642
|
+
db.add_entity("api.target.local", "domain", {"ip": "192.168.1.101", "ports": "443,8443"})
|
|
1643
|
+
|
|
1644
|
+
# Hosts
|
|
1645
|
+
db.add_entity("192.168.1.100", "host", {"hostname": "web.target.local", "os": "Ubuntu 22.04"})
|
|
1646
|
+
db.add_entity("192.168.1.101", "host", {"hostname": "api.target.local", "os": "Debian 11"})
|
|
1647
|
+
db.add_entity("192.168.1.1", "host", {"hostname": "gateway.local", "os": "Linux"})
|
|
1648
|
+
|
|
1649
|
+
# Services
|
|
1650
|
+
db.add_entity("nginx-80", "service", {"name": "nginx", "port": "80", "version": "1.18.0"})
|
|
1651
|
+
db.add_entity("ssh-22", "service", {"name": "openssh", "port": "22", "version": "8.9"})
|
|
1652
|
+
|
|
1653
|
+
# Vulnerabilities
|
|
1654
|
+
db.add_entity("CVE-2023-1234", "cve", {"severity": "high", "cvss": "8.5", "product": "nginx"})
|
|
1655
|
+
db.add_entity("CVE-2024-5678", "cve", {"severity": "critical", "cvss": "9.8", "product": "openssh"})
|
|
1656
|
+
|
|
1657
|
+
# Findings
|
|
1658
|
+
db.add_entity("SQLi-login", "finding", {"severity": "critical", "type": "sqli", "url": "/login"})
|
|
1659
|
+
db.add_entity("XSS-search", "finding", {"severity": "medium", "type": "xss", "url": "/search"})
|
|
1660
|
+
|
|
1661
|
+
# Tools
|
|
1662
|
+
db.add_entity("nmap", "tool", {"category": "recon", "purpose": "port scanning"})
|
|
1663
|
+
db.add_entity("sqlmap", "tool", {"category": "exploitation", "purpose": "SQL injection"})
|
|
1664
|
+
db.add_entity("burpsuite", "tool", {"category": "web", "purpose": "web testing"})
|
|
1665
|
+
|
|
1666
|
+
# Credentials
|
|
1667
|
+
db.add_entity("admin-creds", "credential", {"type": "password", "source": "phishing", "valid": "true"})
|
|
1668
|
+
|
|
1669
|
+
# Exploits
|
|
1670
|
+
db.add_entity("exploit-db-12345", "exploit", {"name": "nginx_rce", "cve": "CVE-2023-1234", "reliability": "high"})
|
|
1671
|
+
|
|
1672
|
+
# Relations
|
|
1673
|
+
db.add_relation("target.local", "has_host", "192.168.1.100")
|
|
1674
|
+
db.add_relation("target.local", "has_host", "192.168.1.101")
|
|
1675
|
+
db.add_relation("web.target.local", "resolves_to", "192.168.1.100")
|
|
1676
|
+
db.add_relation("api.target.local", "resolves_to", "192.168.1.101")
|
|
1677
|
+
db.add_relation("192.168.1.100", "has_service", "nginx-80")
|
|
1678
|
+
db.add_relation("192.168.1.100", "has_service", "ssh-22")
|
|
1679
|
+
db.add_relation("nginx-80", "has_vuln", "CVE-2023-1234")
|
|
1680
|
+
db.add_relation("ssh-22", "has_vuln", "CVE-2024-5678")
|
|
1681
|
+
db.add_relation("web.target.local", "has_vuln", "SQLi-login")
|
|
1682
|
+
db.add_relation("web.target.local", "has_vuln", "XSS-search")
|
|
1683
|
+
db.add_relation("exploit-db-12345", "exploits", "CVE-2023-1234")
|
|
1684
|
+
db.add_relation("sqlmap", "finds", "SQLi-login")
|
|
1685
|
+
db.add_relation("admin-creds", "valid_for", "web.target.local")
|
|
1686
|
+
db.add_relation("192.168.1.1", "part_of_network", "target.local")
|
|
1687
|
+
|
|
1688
|
+
lines.append(" ā Added security entities (targets, hosts, vulns, tools)")
|
|
1689
|
+
|
|
1690
|
+
db.save()
|
|
1691
|
+
|
|
1692
|
+
stats = db.get_stats()
|
|
1693
|
+
lines.append(f"\nā Demo complete!")
|
|
1694
|
+
lines.append(f" Total entities: {stats['total_entities']}")
|
|
1695
|
+
lines.append(f" Total relations: {stats['total_relations']}")
|
|
1696
|
+
lines.append(f"\nTry: /cortex graph")
|
|
1697
|
+
lines.append(f" /cortex query HanusCode 2")
|
|
1698
|
+
lines.append(f" /cortex path Alice Python")
|
|
1699
|
+
|
|
1700
|
+
return "\n".join(lines)
|
|
1701
|
+
|
|
1702
|
+
|
|
1703
|
+
def _cmd_reset(args: str) -> str:
|
|
1704
|
+
"""Reset/clear all cortex data."""
|
|
1705
|
+
db = _get_db()
|
|
1706
|
+
|
|
1707
|
+
if args.strip().lower() == "--force":
|
|
1708
|
+
# Skip confirmation
|
|
1709
|
+
pass
|
|
1710
|
+
elif db.entities:
|
|
1711
|
+
return f"ā ļø This will delete ALL data:\n - {len(db.entities)} entities\n - {len(db.relations)} relations\n - {len(db.events)} events\n\nUse: /cortex reset --force"
|
|
1712
|
+
|
|
1713
|
+
# Clear all data
|
|
1714
|
+
db.entities.clear()
|
|
1715
|
+
db.relations.clear()
|
|
1716
|
+
db.events.clear()
|
|
1717
|
+
db._relation_index.clear()
|
|
1718
|
+
db.save()
|
|
1719
|
+
|
|
1720
|
+
# Clear global nodes in webui
|
|
1721
|
+
return "ā All cortex data has been cleared.\n\nUse '/cortex demo' to generate sample data."
|
|
1722
|
+
|
|
1723
|
+
|
|
1724
|
+
# API for integration with other plugins
|
|
1725
|
+
def get_entity(name: str) -> Optional[dict]:
|
|
1726
|
+
"""API: Get an entity."""
|
|
1727
|
+
return _get_db().get_entity(name)
|
|
1728
|
+
|
|
1729
|
+
|
|
1730
|
+
def add_entity(name: str, entity_type: str, attrs: dict = None) -> bool:
|
|
1731
|
+
"""API: Add an entity."""
|
|
1732
|
+
db = _get_db()
|
|
1733
|
+
result = db.add_entity(name, entity_type, attrs)
|
|
1734
|
+
db.save()
|
|
1735
|
+
return result
|
|
1736
|
+
|
|
1737
|
+
|
|
1738
|
+
def add_relation(from_entity: str, relation: str, to_entity: str) -> bool:
|
|
1739
|
+
"""API: Add a relation."""
|
|
1740
|
+
return _get_db().add_relation(from_entity, relation, to_entity)
|
|
1741
|
+
|
|
1742
|
+
|
|
1743
|
+
def get_related(entity: str, depth: int = 1) -> Set[str]:
|
|
1744
|
+
"""API: Get related entities."""
|
|
1745
|
+
return _get_db().get_related_entities(entity, depth)
|
|
1746
|
+
|
|
1747
|
+
|
|
1748
|
+
def find_path(from_entity: str, to_entity: str) -> Optional[List[Tuple[str, str, str]]]:
|
|
1749
|
+
"""API: Find path between entities."""
|
|
1750
|
+
return _get_db().find_path(from_entity, to_entity)
|