cipher-security 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.
- bot/__init__.py +1 -0
- bot/bot.py +234 -0
- bot/format.py +84 -0
- bot/session.py +98 -0
- cipher_security-0.2.0.dist-info/METADATA +249 -0
- cipher_security-0.2.0.dist-info/RECORD +20 -0
- cipher_security-0.2.0.dist-info/WHEEL +4 -0
- cipher_security-0.2.0.dist-info/entry_points.txt +4 -0
- cipher_security-0.2.0.dist-info/licenses/LICENSE +661 -0
- gateway/__init__.py +8 -0
- gateway/app.py +607 -0
- gateway/cli.py +273 -0
- gateway/client.py +57 -0
- gateway/config.py +213 -0
- gateway/dashboard.py +347 -0
- gateway/gateway.py +171 -0
- gateway/mode.py +127 -0
- gateway/prompt.py +165 -0
- gateway/retriever.py +257 -0
- gateway/theme.py +91 -0
bot/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Bot package β Signal bot utilities.
|
bot/bot.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
# Licensed under AGPL-3.0 β see LICENSE file for details.
|
|
3
|
+
"""CIPHER Signal bot β CipherCommand handler and watchdog main loop.
|
|
4
|
+
|
|
5
|
+
Entry point for the Signal bot container. Registers CipherCommand with
|
|
6
|
+
signalbot, filters by whitelist, maps prefixes, dispatches to Gateway via
|
|
7
|
+
executor, strips markdown, and manages session history.
|
|
8
|
+
|
|
9
|
+
Watchdog: outer while-True retries bot.start() on any exception with
|
|
10
|
+
exponential backoff (1s base, 2x multiplier, 60s max).
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import logging
|
|
16
|
+
import time
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from signalbot import Command # type: ignore[import]
|
|
20
|
+
|
|
21
|
+
from bot.format import map_prefix, strip_markdown
|
|
22
|
+
from bot.session import SessionManager
|
|
23
|
+
from gateway.gateway import Gateway
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# Disambiguation keyword map
|
|
28
|
+
_CLARIFY_MAP: dict[str, str] = {
|
|
29
|
+
"offensive": "RED",
|
|
30
|
+
"defensive": "BLUE",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CipherCommand(Command):
|
|
35
|
+
"""signalbot Command handler for CIPHER.
|
|
36
|
+
|
|
37
|
+
Filters senders against a whitelist, maps short prefixes, dispatches to
|
|
38
|
+
Gateway via asyncio executor (non-blocking), strips markdown, manages
|
|
39
|
+
session history, and handles disambiguation flow.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
gateway: Constructed Gateway instance.
|
|
43
|
+
session: SessionManager for conversation history.
|
|
44
|
+
whitelist: Set of E.164 phone numbers allowed to use the bot.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
gateway: Gateway,
|
|
50
|
+
session: SessionManager,
|
|
51
|
+
whitelist: set[str],
|
|
52
|
+
) -> None:
|
|
53
|
+
super().__init__()
|
|
54
|
+
self._gateway = gateway
|
|
55
|
+
self._session = session
|
|
56
|
+
self._whitelist = whitelist
|
|
57
|
+
# Keyed by sender E.164 number; value is the original query awaiting
|
|
58
|
+
# clarification (offensive vs defensive).
|
|
59
|
+
self._clarification: dict[str, str] = {}
|
|
60
|
+
|
|
61
|
+
async def handle(self, c: Any) -> None:
|
|
62
|
+
"""Handle an incoming Signal message.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
c: signalbot Context (or MockContext in tests).
|
|
66
|
+
"""
|
|
67
|
+
text: str = (c.message.text or "").strip()
|
|
68
|
+
|
|
69
|
+
# --- Skip empty messages (typing indicators, system events) ---
|
|
70
|
+
if not text:
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
# --- Whitelist gate ---
|
|
74
|
+
# Signal may identify senders by UUID or phone number depending on
|
|
75
|
+
# privacy settings. Check all available identifiers.
|
|
76
|
+
sender_ids = {c.message.source, c.message.source_uuid}
|
|
77
|
+
if c.message.source_number:
|
|
78
|
+
sender_ids.add(c.message.source_number)
|
|
79
|
+
if not sender_ids & self._whitelist:
|
|
80
|
+
return
|
|
81
|
+
sender: str = c.message.source_number or c.message.source
|
|
82
|
+
|
|
83
|
+
# --- /reset command ---
|
|
84
|
+
if text.strip() == "/reset":
|
|
85
|
+
self._session.reset(sender)
|
|
86
|
+
await c.send("Session cleared.")
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
# --- Disambiguation reply check ---
|
|
90
|
+
if sender in self._clarification:
|
|
91
|
+
await self._handle_clarification(c, sender, text)
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
# --- Acknowledge with brain emoji ---
|
|
95
|
+
await c.react("π§ ")
|
|
96
|
+
|
|
97
|
+
# --- Map short prefix ---
|
|
98
|
+
mapped = map_prefix(text)
|
|
99
|
+
|
|
100
|
+
# --- Fetch session history ---
|
|
101
|
+
history = self._session.get(sender)
|
|
102
|
+
|
|
103
|
+
# --- Dispatch to Gateway via executor (non-blocking) ---
|
|
104
|
+
loop = asyncio.get_running_loop()
|
|
105
|
+
response: str = await loop.run_in_executor(
|
|
106
|
+
None,
|
|
107
|
+
lambda: self._gateway.send(mapped, history=history),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# --- Disambiguation: multiple modes detected ---
|
|
111
|
+
if response.startswith("Multiple modes detected"):
|
|
112
|
+
self._clarification[sender] = mapped
|
|
113
|
+
await c.send(response)
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# --- Update session and send response ---
|
|
117
|
+
self._session.update(sender, mapped, response)
|
|
118
|
+
await c.send(strip_markdown(response))
|
|
119
|
+
|
|
120
|
+
async def _handle_clarification(
|
|
121
|
+
self, c: Any, sender: str, reply: str
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Process a clarification reply (offensive / defensive).
|
|
124
|
+
|
|
125
|
+
Maps the reply to a mode prefix, re-routes the stored query.
|
|
126
|
+
Keeps state if the reply is unrecognized.
|
|
127
|
+
"""
|
|
128
|
+
reply_lower = reply.strip().lower()
|
|
129
|
+
|
|
130
|
+
# Find matching mode keyword
|
|
131
|
+
matched_mode: str | None = None
|
|
132
|
+
for keyword, mode in _CLARIFY_MAP.items():
|
|
133
|
+
if keyword in reply_lower:
|
|
134
|
+
matched_mode = mode
|
|
135
|
+
break
|
|
136
|
+
|
|
137
|
+
if matched_mode is None:
|
|
138
|
+
await c.send("Please reply 'offensive' or 'defensive'.")
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
# Route stored query with resolved prefix
|
|
142
|
+
original_query = self._clarification.pop(sender)
|
|
143
|
+
prefixed = f"[MODE: {matched_mode}] {original_query}"
|
|
144
|
+
|
|
145
|
+
await c.react("π§ ")
|
|
146
|
+
|
|
147
|
+
history = self._session.get(sender)
|
|
148
|
+
loop = asyncio.get_running_loop()
|
|
149
|
+
response: str = await loop.run_in_executor(
|
|
150
|
+
None,
|
|
151
|
+
lambda: self._gateway.send(prefixed, history=history),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
self._session.update(sender, prefixed, response)
|
|
155
|
+
await c.send(strip_markdown(response))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
# Bot runner
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
def run_bot(config: dict[str, Any]) -> None:
|
|
163
|
+
"""Construct and start the SignalBot with CipherCommand registered.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
config: Signal config dict (signal_service, phone_number, whitelist,
|
|
167
|
+
session_timeout).
|
|
168
|
+
"""
|
|
169
|
+
from signalbot import SignalBot, Config # type: ignore[import]
|
|
170
|
+
|
|
171
|
+
signal_cfg = Config(
|
|
172
|
+
signal_service=config["signal_service"],
|
|
173
|
+
phone_number=config["phone_number"],
|
|
174
|
+
)
|
|
175
|
+
bot = SignalBot(signal_cfg)
|
|
176
|
+
gateway = Gateway()
|
|
177
|
+
session = SessionManager(
|
|
178
|
+
timeout_seconds=int(config.get("session_timeout", 3600))
|
|
179
|
+
)
|
|
180
|
+
cmd = CipherCommand(
|
|
181
|
+
gateway=gateway,
|
|
182
|
+
session=session,
|
|
183
|
+
whitelist=set(config.get("whitelist", [])),
|
|
184
|
+
)
|
|
185
|
+
bot.register(cmd)
|
|
186
|
+
bot.start()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
# Main with watchdog
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
def main() -> None:
|
|
194
|
+
"""Entry point: load config, run bot with exponential-backoff watchdog."""
|
|
195
|
+
logging.basicConfig(
|
|
196
|
+
level=logging.INFO,
|
|
197
|
+
format="%(asctime)s %(levelname)s %(name)s β %(message)s",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
from gateway.config import _find_project_root, _load_yaml_file
|
|
201
|
+
from pathlib import Path
|
|
202
|
+
|
|
203
|
+
# Locate config.yaml
|
|
204
|
+
project_root = _find_project_root(Path(__file__).parent)
|
|
205
|
+
if project_root is None:
|
|
206
|
+
project_root = Path.cwd()
|
|
207
|
+
|
|
208
|
+
raw = _load_yaml_file(Path(project_root) / "config.yaml")
|
|
209
|
+
signal_cfg: dict[str, Any] = raw.get("signal", {})
|
|
210
|
+
|
|
211
|
+
# Env var overrides
|
|
212
|
+
import os
|
|
213
|
+
if sv := os.environ.get("SIGNAL_SERVICE"):
|
|
214
|
+
signal_cfg["signal_service"] = sv
|
|
215
|
+
if spn := os.environ.get("SIGNAL_PHONE_NUMBER"):
|
|
216
|
+
signal_cfg["phone_number"] = spn
|
|
217
|
+
if swl := os.environ.get("SIGNAL_WHITELIST"):
|
|
218
|
+
signal_cfg["whitelist"] = [n.strip() for n in swl.split(",") if n.strip()]
|
|
219
|
+
|
|
220
|
+
delay = 1.0
|
|
221
|
+
while True:
|
|
222
|
+
try:
|
|
223
|
+
logger.info("Starting bot β¦")
|
|
224
|
+
run_bot(signal_cfg)
|
|
225
|
+
# Clean exit β reset delay
|
|
226
|
+
delay = 1.0
|
|
227
|
+
except Exception as exc: # noqa: BLE001
|
|
228
|
+
logger.error("Bot crashed: %s β retrying in %.0fs", exc, delay)
|
|
229
|
+
time.sleep(delay)
|
|
230
|
+
delay = min(delay * 2, 60.0)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
if __name__ == "__main__":
|
|
234
|
+
main()
|
bot/format.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
# Licensed under AGPL-3.0 β see LICENSE file for details.
|
|
3
|
+
"""Bot formatting utilities.
|
|
4
|
+
|
|
5
|
+
map_prefix: Maps short Signal prefix (e.g. 'RED: query') to gateway format
|
|
6
|
+
('[MODE: RED] query'). Passes through text with no recognized prefix.
|
|
7
|
+
|
|
8
|
+
strip_markdown: Strips markdown formatting from LLM responses for plaintext
|
|
9
|
+
Signal delivery. Preserves structure via line breaks and ASCII bullets.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Prefix mapping
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
_PREFIX_RE = re.compile(
|
|
19
|
+
r"^(RED|BLUE|PURPLE|PRIVACY|RECON|INCIDENT|ARCHITECT):\s*",
|
|
20
|
+
re.IGNORECASE,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def map_prefix(text: str) -> str:
|
|
25
|
+
"""Map 'MODE: query' short-prefix format to '[MODE: MODE] query'.
|
|
26
|
+
|
|
27
|
+
Case-insensitive. Strips optional whitespace after colon.
|
|
28
|
+
Returns text unchanged when no recognized mode prefix is found.
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
map_prefix("RED: query") -> "[MODE: RED] query"
|
|
32
|
+
map_prefix("blue: what is DNS") -> "[MODE: BLUE] what is DNS"
|
|
33
|
+
map_prefix("RED:query") -> "[MODE: RED] query"
|
|
34
|
+
map_prefix("no prefix here") -> "no prefix here"
|
|
35
|
+
"""
|
|
36
|
+
m = _PREFIX_RE.match(text)
|
|
37
|
+
if m:
|
|
38
|
+
mode = m.group(1).upper()
|
|
39
|
+
rest = text[m.end():]
|
|
40
|
+
return f"[MODE: {mode}] {rest}"
|
|
41
|
+
return text
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Markdown stripping
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
# Patterns applied in order β order matters (bold before italic, code blocks
|
|
49
|
+
# before inline code).
|
|
50
|
+
_MD_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
51
|
+
# Fenced code blocks: ```lang\ncontent\n``` -> content
|
|
52
|
+
(re.compile(r"```[^\n]*\n(.*?)```", re.DOTALL), r"\1"),
|
|
53
|
+
# Headers: ## Heading -> Heading
|
|
54
|
+
(re.compile(r"^#{1,6}\s+", re.MULTILINE), ""),
|
|
55
|
+
# Bold: **text** -> text (must be before italic)
|
|
56
|
+
(re.compile(r"\*{2}([^*\n]+)\*{2}"), r"\1"),
|
|
57
|
+
# Italic: *text* -> text
|
|
58
|
+
(re.compile(r"\*([^*\n]+)\*"), r"\1"),
|
|
59
|
+
# Inline code: `code` -> code
|
|
60
|
+
(re.compile(r"`([^`\n]+)`"), r"\1"),
|
|
61
|
+
# Links: [text](url) -> text
|
|
62
|
+
(re.compile(r"\[([^\]]+)\]\([^)]+\)"), r"\1"),
|
|
63
|
+
# Bullets: "* item" or "- item" -> "β’ item" (preserves leading whitespace)
|
|
64
|
+
(re.compile(r"^(\s*)[*-]\s+", re.MULTILINE), r"\1β’ "),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def strip_markdown(text: str) -> str:
|
|
69
|
+
"""Strip markdown formatting for plaintext Signal delivery.
|
|
70
|
+
|
|
71
|
+
Removes headers, bold, italic, code blocks, inline code, links.
|
|
72
|
+
Converts markdown bullets (* / -) to ASCII bullets (β’).
|
|
73
|
+
Preserves plain text, newlines, and indentation.
|
|
74
|
+
|
|
75
|
+
Examples:
|
|
76
|
+
strip_markdown("## Heading") -> "Heading"
|
|
77
|
+
strip_markdown("**bold** text") -> "bold text"
|
|
78
|
+
strip_markdown("```python\\ncode\\n```") -> "code"
|
|
79
|
+
strip_markdown("* item") -> "β’ item"
|
|
80
|
+
strip_markdown("[link](http://x)") -> "link"
|
|
81
|
+
"""
|
|
82
|
+
for pattern, replacement in _MD_PATTERNS:
|
|
83
|
+
text = pattern.sub(replacement, text)
|
|
84
|
+
return text.strip()
|
bot/session.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
# Licensed under AGPL-3.0 β see LICENSE file for details.
|
|
3
|
+
"""Session management for the CIPHER Signal bot.
|
|
4
|
+
|
|
5
|
+
SessionManager maintains per-sender conversation history compatible with
|
|
6
|
+
Gateway.send(history=) format. Sessions expire after configurable inactivity
|
|
7
|
+
and are capped at a maximum number of message pairs.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from datetime import datetime, timezone, timedelta
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SessionManager:
|
|
16
|
+
"""In-memory conversation history manager keyed by sender phone number.
|
|
17
|
+
|
|
18
|
+
Session dict structure per sender:
|
|
19
|
+
{
|
|
20
|
+
"history": [{"role": "user"|"assistant", "content": "..."}, ...],
|
|
21
|
+
"last_active": datetime (UTC),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
History format matches Gateway.send(history=) β list of role/content dicts.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
timeout_seconds: Inactivity window before a session is expired.
|
|
28
|
+
Default 3600 (1 hour).
|
|
29
|
+
max_pairs: Maximum number of user/assistant pairs to retain.
|
|
30
|
+
Oldest pair is evicted when the cap is exceeded. Default 20.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, timeout_seconds: int = 3600, max_pairs: int = 20) -> None:
|
|
34
|
+
self._sessions: dict[str, dict] = {}
|
|
35
|
+
self._timeout = timeout_seconds
|
|
36
|
+
self._max_pairs = max_pairs
|
|
37
|
+
|
|
38
|
+
# ------------------------------------------------------------------
|
|
39
|
+
# Public API
|
|
40
|
+
# ------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
def get(self, sender: str) -> list[dict]:
|
|
43
|
+
"""Return conversation history for sender.
|
|
44
|
+
|
|
45
|
+
Calls cleanup() first to expire stale sessions.
|
|
46
|
+
Returns empty list for unknown or expired senders.
|
|
47
|
+
"""
|
|
48
|
+
self.cleanup()
|
|
49
|
+
session = self._sessions.get(sender)
|
|
50
|
+
if session is None:
|
|
51
|
+
return []
|
|
52
|
+
return list(session["history"])
|
|
53
|
+
|
|
54
|
+
def update(self, sender: str, user_message: str, assistant_message: str) -> None:
|
|
55
|
+
"""Append a user/assistant pair to sender's history.
|
|
56
|
+
|
|
57
|
+
Creates the session if it does not exist. Updates last_active.
|
|
58
|
+
Evicts the oldest pair if the cap is exceeded.
|
|
59
|
+
"""
|
|
60
|
+
if sender not in self._sessions:
|
|
61
|
+
self._sessions[sender] = {"history": [], "last_active": self._now()}
|
|
62
|
+
|
|
63
|
+
session = self._sessions[sender]
|
|
64
|
+
session["history"].append({"role": "user", "content": user_message})
|
|
65
|
+
session["history"].append({"role": "assistant", "content": assistant_message})
|
|
66
|
+
session["last_active"] = self._now()
|
|
67
|
+
|
|
68
|
+
# Enforce cap: keep only the newest max_pairs pairs (max_pairs * 2 items)
|
|
69
|
+
max_items = self._max_pairs * 2
|
|
70
|
+
if len(session["history"]) > max_items:
|
|
71
|
+
session["history"] = session["history"][-max_items:]
|
|
72
|
+
|
|
73
|
+
def reset(self, sender: str) -> None:
|
|
74
|
+
"""Clear conversation history for sender.
|
|
75
|
+
|
|
76
|
+
No-op if sender has no active session.
|
|
77
|
+
"""
|
|
78
|
+
self._sessions.pop(sender, None)
|
|
79
|
+
|
|
80
|
+
def cleanup(self) -> None:
|
|
81
|
+
"""Remove sessions that have been inactive longer than timeout_seconds."""
|
|
82
|
+
now = self._now()
|
|
83
|
+
cutoff = timedelta(seconds=self._timeout)
|
|
84
|
+
stale = [
|
|
85
|
+
sender
|
|
86
|
+
for sender, session in self._sessions.items()
|
|
87
|
+
if (now - session["last_active"]) > cutoff
|
|
88
|
+
]
|
|
89
|
+
for sender in stale:
|
|
90
|
+
del self._sessions[sender]
|
|
91
|
+
|
|
92
|
+
# ------------------------------------------------------------------
|
|
93
|
+
# Internal helpers
|
|
94
|
+
# ------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _now() -> datetime:
|
|
98
|
+
return datetime.now(timezone.utc)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cipher-security
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: CIPHER β Security Engineering Assistant with RAG-powered knowledge base
|
|
5
|
+
Project-URL: Homepage, https://github.com/defconxt/CIPHER
|
|
6
|
+
Project-URL: Documentation, https://blacktemple.net/cipher
|
|
7
|
+
Project-URL: Repository, https://github.com/defconxt/CIPHER
|
|
8
|
+
Project-URL: Issues, https://github.com/defconxt/CIPHER/issues
|
|
9
|
+
Author-email: defconxt <trevor@blacktemple.net>
|
|
10
|
+
License-Expression: AGPL-3.0-only
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ATT&CK,SIEM,cybersecurity,detection,pentest,security,threat-hunting
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Information Technology
|
|
16
|
+
Classifier: Intended Audience :: System Administrators
|
|
17
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Security
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: anthropic>=0.84.0
|
|
27
|
+
Requires-Dist: chromadb>=1.0.0
|
|
28
|
+
Requires-Dist: pyyaml>=6.0
|
|
29
|
+
Requires-Dist: rich>=13.0
|
|
30
|
+
Requires-Dist: textual>=1.0
|
|
31
|
+
Requires-Dist: typer>=0.12
|
|
32
|
+
Provides-Extra: all
|
|
33
|
+
Requires-Dist: signalbot>=0.25.0; extra == 'all'
|
|
34
|
+
Provides-Extra: signal
|
|
35
|
+
Requires-Dist: signalbot>=0.25.0; extra == 'signal'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
<!-- Copyright (c) 2026 defconxt. All rights reserved. -->
|
|
39
|
+
<!-- Licensed under AGPL-3.0 β see LICENSE file for details. -->
|
|
40
|
+
<!-- CIPHER is a trademark of defconxt. -->
|
|
41
|
+
|
|
42
|
+
<div align="center">
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
βββββββββββββββββ βββ ββββββββββββββββββ
|
|
46
|
+
ββββββββββββββββββββββ βββββββββββββββββββ
|
|
47
|
+
βββ βββββββββββββββββββββββββ ββββββββ
|
|
48
|
+
βββ ββββββββββ ββββββββββββββ ββββββββ
|
|
49
|
+
ββββββββββββββ βββ ββββββββββββββ βββ
|
|
50
|
+
βββββββββββββ βββ ββββββββββββββ βββ
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Security Engineering Assistant**
|
|
54
|
+
|
|
55
|
+
[](LICENSE)
|
|
56
|
+
[](https://github.com/defconxt/CIPHER/actions)
|
|
57
|
+
[](https://python.org)
|
|
58
|
+
|
|
59
|
+
A principal-level security engineering assistant with 96 deep-dive knowledge docs,
|
|
60
|
+
RAG-powered retrieval, 7 operating modes, and 28 specialized skills.
|
|
61
|
+
Runs locally via Ollama or cloud via Claude API.
|
|
62
|
+
|
|
63
|
+
[Install](#install) Β· [Quick Start](#quick-start) Β· [Features](#features) Β· [Documentation](https://blacktemple.net/cipher)
|
|
64
|
+
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
|
|
71
|
+
**One-liner (Linux/macOS):**
|
|
72
|
+
```bash
|
|
73
|
+
curl -fsSL https://blacktemple.net/cipher/install.sh | bash
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Windows PowerShell:**
|
|
77
|
+
```powershell
|
|
78
|
+
iwr -useb https://blacktemple.net/cipher/install.ps1 | iex
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
> **Note:** Install scripts also available at `https://raw.githubusercontent.com/defconxt/CIPHER/main/scripts/install.sh`
|
|
82
|
+
|
|
83
|
+
**pip:**
|
|
84
|
+
```bash
|
|
85
|
+
pip install cipher-security
|
|
86
|
+
cipher setup
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Docker:**
|
|
90
|
+
```bash
|
|
91
|
+
docker run -it ghcr.io/defconxt/cipher "how do I detect Kerberoasting"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Quick Start
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Ask anything security
|
|
98
|
+
cipher "how do I detect lateral movement via PsExec"
|
|
99
|
+
|
|
100
|
+
# Force a specific mode
|
|
101
|
+
cipher "[MODE: RED] exploit AS-REP roasting in Active Directory"
|
|
102
|
+
|
|
103
|
+
# Auto-route: simple queries β local Ollama, complex β Claude API
|
|
104
|
+
cipher --smart "design a zero trust architecture for multi-cloud"
|
|
105
|
+
|
|
106
|
+
# Interactive dashboard
|
|
107
|
+
cipher dashboard
|
|
108
|
+
|
|
109
|
+
# System health check
|
|
110
|
+
cipher doctor
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Features
|
|
114
|
+
|
|
115
|
+
### 7 Operating Modes
|
|
116
|
+
|
|
117
|
+
| Mode | Focus | Triggers |
|
|
118
|
+
|------|-------|----------|
|
|
119
|
+
| **RED** | Offensive security, attack paths, exploitation | exploit, payload, privesc, C2, red team |
|
|
120
|
+
| **BLUE** | Detection, SIEM, hardening, threat hunting | detection, Sigma, SIEM, EDR, hardening |
|
|
121
|
+
| **PURPLE** | Adversary emulation, coverage mapping | emulation, ATT&CK mapping, gap analysis |
|
|
122
|
+
| **PRIVACY** | GDPR, CCPA, HIPAA, DPIAs, anonymization | GDPR, CCPA, HIPAA, DPIA, privacy |
|
|
123
|
+
| **RECON** | OSINT, reconnaissance, footprinting | OSINT, recon, subdomain, footprinting |
|
|
124
|
+
| **INCIDENT** | IR playbooks, forensics, containment | triage, incident response, IOC, forensics |
|
|
125
|
+
| **ARCHITECT** | Zero trust, threat modeling, design | design, architecture, threat model, zero trust |
|
|
126
|
+
|
|
127
|
+
Modes auto-detect from your query. Ambiguous queries prompt for clarification.
|
|
128
|
+
|
|
129
|
+
### 96 Deep-Dive Knowledge Docs
|
|
130
|
+
|
|
131
|
+
RAG-powered retrieval over 145K+ lines of security knowledge:
|
|
132
|
+
|
|
133
|
+
- **Offensive**: AD attacks, web exploitation, cloud attacks, C2, shells, evasion techniques
|
|
134
|
+
- **Defensive**: Sigma rules, SIEM/SOC, EDR internals, hardening guides, Windows Event Log mastery
|
|
135
|
+
- **Forensics**: DFIR hunting, forensic artifacts, timeline analysis, memory forensics, email forensics
|
|
136
|
+
- **Architecture**: Threat modeling (STRIDE/PASTA), cloud reference architectures, crypto/PKI/TLS
|
|
137
|
+
- **Compliance**: NIST, CIS, SOC2, ISO 27001, GDPR, PCI DSS, HIPAA
|
|
138
|
+
- **Emerging**: Breach case studies (12 major incidents), AI/ML security, vulnerability research, ransomware ecosystem
|
|
139
|
+
|
|
140
|
+
### 28 Slash Commands (Claude Code)
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
/cipher:redteam /cipher:web /cipher:phishing /cipher:malware
|
|
144
|
+
/cipher:hunt /cipher:sigma /cipher:hardening /cipher:forensics
|
|
145
|
+
/cipher:purple /cipher:recon /cipher:privacy /cipher:insider
|
|
146
|
+
/cipher:threatmodel /cipher:cloud /cipher:crypto /cipher:devsecops
|
|
147
|
+
/cipher:ir /cipher:cve /cipher:threatintel /cipher:aisec
|
|
148
|
+
/cipher:audit /cipher:report /cipher:ics /cipher:mobile
|
|
149
|
+
/cipher:cc /cipher:ollama /cipher:claudeapi /cipher:smart
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Multiple Interfaces
|
|
153
|
+
|
|
154
|
+
| Interface | Command | Cost |
|
|
155
|
+
|-----------|---------|------|
|
|
156
|
+
| **CLI** | `cipher "query"` | Free (Ollama) or paid (Claude) |
|
|
157
|
+
| **Dashboard** | `cipher dashboard` | Free (Ollama) or paid (Claude) |
|
|
158
|
+
| **Claude Code** | `/cipher:cc "query"` | Included with Claude Code |
|
|
159
|
+
| **Signal Bot** | Message the bot | Free (Ollama) or paid (Claude) |
|
|
160
|
+
| **Docker** | `docker run -it cipher "query"` | Free (Ollama) or paid (Claude) |
|
|
161
|
+
|
|
162
|
+
### RAG Pipeline
|
|
163
|
+
|
|
164
|
+
Semantic search over the entire knowledge base:
|
|
165
|
+
- **14,600+ chunks** indexed in ChromaDB
|
|
166
|
+
- Markdown-aware chunking preserving section context
|
|
167
|
+
- Top-5 retrieval injected into system prompt before every query
|
|
168
|
+
- Auto re-ingests on knowledge base changes (git hook)
|
|
169
|
+
|
|
170
|
+
## CLI Commands
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
cipher "query" Send a security query (default command)
|
|
174
|
+
cipher --smart "query" Auto-route: simpleβOllama, complexβClaude
|
|
175
|
+
cipher --backend ollama Force local inference
|
|
176
|
+
cipher --backend claude Force cloud inference
|
|
177
|
+
cipher setup Interactive onboarding wizard
|
|
178
|
+
cipher dashboard Cyberpunk terminal UI
|
|
179
|
+
cipher doctor Diagnostics and health checks
|
|
180
|
+
cipher doctor --fix Auto-repair issues
|
|
181
|
+
cipher status Show system status
|
|
182
|
+
cipher ingest Re-index the knowledge base
|
|
183
|
+
cipher version Show version
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Architecture
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
190
|
+
β Interfaces β
|
|
191
|
+
β CLI Β· Dashboard Β· Claude Code Β· Signal Bot Β· Docker β
|
|
192
|
+
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββ
|
|
193
|
+
β
|
|
194
|
+
ββββββββββΌβββββββββ
|
|
195
|
+
β Gateway β
|
|
196
|
+
β Mode Detection βββββ 7 modes
|
|
197
|
+
β Prompt Assemblyβββββ 13 skill files
|
|
198
|
+
β RAG Retrieval βββββ 14,600 chunks
|
|
199
|
+
ββββββββββ¬βββββββββ
|
|
200
|
+
β
|
|
201
|
+
βββββββββββββΌββββββββββββ
|
|
202
|
+
β β
|
|
203
|
+
ββββββββΌβββββββ ββββββββΌβββββββ
|
|
204
|
+
β Ollama β β Claude API β
|
|
205
|
+
β (local) β β (cloud) β
|
|
206
|
+
β qwen2.5:32b β β sonnet-4-5 β
|
|
207
|
+
βββββββββββββββ βββββββββββββββ
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Configuration
|
|
211
|
+
|
|
212
|
+
Config file: `config.yaml` (project root) or `~/.config/cipher/config.yaml`
|
|
213
|
+
|
|
214
|
+
Supports `${VAR}` and `${VAR:-default}` environment variable substitution:
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
llm_backend: ollama
|
|
218
|
+
|
|
219
|
+
ollama:
|
|
220
|
+
base_url: "http://127.0.0.1:11434"
|
|
221
|
+
model: "cipher"
|
|
222
|
+
timeout: 300
|
|
223
|
+
|
|
224
|
+
claude:
|
|
225
|
+
api_key: "${ANTHROPIC_API_KEY}"
|
|
226
|
+
model: "claude-sonnet-4-5-20250929"
|
|
227
|
+
timeout: 120
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Priority: env vars > project config > home config.
|
|
231
|
+
|
|
232
|
+
## Development
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
git clone https://github.com/defconxt/CIPHER.git && cd CIPHER
|
|
236
|
+
python -m venv .venv && source .venv/bin/activate
|
|
237
|
+
pip install -e ".[signal]"
|
|
238
|
+
pytest tests/ --ignore=tests/test_integration.py
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**318 tests** covering gateway, mode routing, RAG quality, architecture guardrails, and configuration.
|
|
242
|
+
|
|
243
|
+
## Contributing
|
|
244
|
+
|
|
245
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. Security researchers welcome.
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
[AGPL-3.0](LICENSE) β CIPHER is a trademark of [defconxt](https://github.com/defconxt).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
bot/__init__.py,sha256=YCaNS9AnlUTyxgrZc9bPrLDq73Eo8fuOV0vOCsNl2WQ,40
|
|
2
|
+
bot/bot.py,sha256=44GAM6yNz7Cv-vMuYTvBiRh9hcnF8k81Y9wo6wcABAQ,7589
|
|
3
|
+
bot/format.py,sha256=GMmhzUczKsRkM571GIcAMOjV1RbKDeYP4VyXIcyNU8c,3177
|
|
4
|
+
bot/session.py,sha256=KZe8l0qsEYDuGYIaTvyLE5MLMvRmHO1vsZOaTJSjzUY,3609
|
|
5
|
+
gateway/__init__.py,sha256=Fcp_qiUotdefmhau1Hx5QteL5naU883v77wqHqsBVUw,347
|
|
6
|
+
gateway/app.py,sha256=prVguxv5AHDL2Yirb75m85lJu0dF9ojzmpSO42RuD1A,22208
|
|
7
|
+
gateway/cli.py,sha256=IWji6x-EipmOGdHoqjy-4BTO3C-8BbM9c6cXr4xy5qw,9881
|
|
8
|
+
gateway/client.py,sha256=GY8a9ZG4R6bJXB-Yp-_AtNDMyP5R7Dd-ANb1jYBmddo,1968
|
|
9
|
+
gateway/config.py,sha256=JBM2jURybnk3X9KuaRganQ9PlSBX_MM2Z-5iPXWgy0A,7757
|
|
10
|
+
gateway/dashboard.py,sha256=CATXWb7laVgj9qI37qSH1MmJJL6SopMQn5MWXWMBonI,11007
|
|
11
|
+
gateway/gateway.py,sha256=0Glq-4BVT60HDUeAcZbNJYrfLdOhd5I6jpVyh5MdfdA,6783
|
|
12
|
+
gateway/mode.py,sha256=aYkbFIK4PZNpirm6EhHWZcoeqj1DhRHmntbjaKQLJ-I,4652
|
|
13
|
+
gateway/prompt.py,sha256=C6vrkWlsbCEwkSsy6AGOmtL_bdXhBthM5taQj9HoXDE,5408
|
|
14
|
+
gateway/retriever.py,sha256=XL_td4qe2hBfQbbBeDW537KHOSdgKMkmiDkt7YdW5nU,8776
|
|
15
|
+
gateway/theme.py,sha256=mlXKIJBkvIaHqnm4nw_-1tgCiaRrUnC-b7rp0qRNSLU,3471
|
|
16
|
+
cipher_security-0.2.0.dist-info/METADATA,sha256=g1_g2QTYFPrd8KxhUJ_1zOsWeJnAAQcH6cNX91VJ2wg,9835
|
|
17
|
+
cipher_security-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
18
|
+
cipher_security-0.2.0.dist-info/entry_points.txt,sha256=7NpFwFl9KHaGgHRCboynB1itMHkTHe2YLXzH4GHCHq8,99
|
|
19
|
+
cipher_security-0.2.0.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
|
|
20
|
+
cipher_security-0.2.0.dist-info/RECORD,,
|