diff --git a/Cargo.lock b/Cargo.lock
index 48cb3d4ed..cfdb3d46b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1465,6 +1465,7 @@ dependencies = [
"regex",
"reqwest 0.12.24",
"rhai",
+ "ring 0.17.14",
"rust_xlsxwriter",
"rustls 0.21.12",
"rustls-native-certs 0.6.3",
diff --git a/Cargo.toml b/Cargo.toml
index ddfe55433..c5e7c5ad7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -151,6 +151,7 @@ rcgen = { version = "0.11", features = ["pem"] }
x509-parser = "0.15"
rustls-native-certs = "0.6"
webpki-roots = "0.25"
+ring = "0.17"
time = { version = "0.3", features = ["formatting", "parsing"] }
jsonwebtoken = "9.3"
tower-cookies = "0.10"
diff --git a/docs/book.toml b/docs/book.toml
new file mode 100644
index 000000000..1ab88b9e4
--- /dev/null
+++ b/docs/book.toml
@@ -0,0 +1,19 @@
+[book]
+title = "General Bots Documentation"
+authors = ["General Bots Team"]
+language = "en"
+multilingual = false
+src = "src"
+
+[build]
+build-dir = "book"
+
+[output.html]
+default-theme = "light"
+preferred-dark-theme = "navy"
+smart-punctuation = true
+additional-css = ["src/custom.css", "src/whatsapp-chat.css"]
+additional-js = ["src/theme-sync.js"]
+
+[output.html.favicon]
+png = "favicon.png"
diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md
index 4642cd460..4179456fd 100644
--- a/docs/src/SUMMARY.md
+++ b/docs/src/SUMMARY.md
@@ -317,5 +317,7 @@
- [Appendix C: Environment Variables](./appendix-env-vars/README.md)
+- [Appendix D: Documentation Style](./appendix-docs-style/conversation-examples.md)
+
[Glossary](./glossary.md)
[Contact](./contact/README.md)
\ No newline at end of file
diff --git a/docs/src/appendix-docs-style/conversation-examples.md b/docs/src/appendix-docs-style/conversation-examples.md
new file mode 100644
index 000000000..12f0e5e16
--- /dev/null
+++ b/docs/src/appendix-docs-style/conversation-examples.md
@@ -0,0 +1,235 @@
+# Conversation Examples Style Guide
+
+> **Standard format for displaying bot-user conversations in documentation**
+
+## Overview
+
+All conversation examples in General Bots documentation use a WhatsApp-style chat format. This provides a consistent, familiar, and readable way to show bot interactions.
+
+## CSS Include
+
+The styling is defined in `/assets/wa-chat.css`. Include it in your mdBook or HTML output.
+
+---
+
+## Basic Structure
+
+```html
+
elements.
+ *
+ * Usage:
+ *
+ *
+ *
+ *
Bot message here
+ *
10:30
+ *
+ *
+ *
+ *
+ *
User message here
+ *
10:31
+ *
+ *
+ *
+ */
+
+/* ============================================
+ Chat Container
+ ============================================ */
+.wa-chat {
+ background-color: #e5ddd5;
+ border-radius: 8px;
+ padding: 20px 15px;
+ margin: 20px 0;
+ max-width: 500px;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
+ font-size: 14px;
+}
+
+/* Dark mode support */
+@media (prefers-color-scheme: dark) {
+ .wa-chat {
+ background-color: #0b141a;
+ }
+}
+
+/* ============================================
+ Message Container
+ ============================================ */
+.wa-message {
+ margin-bottom: 10px;
+}
+
+.wa-message:last-child {
+ margin-bottom: 0;
+}
+
+/* User messages align right */
+.wa-message.user {
+ text-align: right;
+}
+
+/* ============================================
+ Message Bubbles
+ ============================================ */
+.wa-bubble {
+ padding: 8px 12px;
+ border-radius: 8px;
+ box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.13);
+ max-width: 85%;
+ display: inline-block;
+ text-align: left;
+}
+
+/* Bot bubble - white/light */
+.wa-message.bot .wa-bubble {
+ background-color: #fff;
+}
+
+/* User bubble - green (WhatsApp style) */
+.wa-message.user .wa-bubble {
+ background-color: #dcf8c6;
+}
+
+/* Dark mode bubbles */
+@media (prefers-color-scheme: dark) {
+ .wa-message.bot .wa-bubble {
+ background-color: #202c33;
+ }
+
+ .wa-message.user .wa-bubble {
+ background-color: #005c4b;
+ }
+}
+
+/* ============================================
+ Message Text
+ ============================================ */
+.wa-bubble p {
+ margin: 0 0 4px 0;
+ line-height: 1.4;
+ color: #303030;
+}
+
+.wa-bubble p:last-of-type {
+ margin-bottom: 0;
+}
+
+/* Bold text in messages */
+.wa-bubble strong {
+ font-weight: 600;
+}
+
+/* Dark mode text */
+@media (prefers-color-scheme: dark) {
+ .wa-bubble p {
+ color: #e9edef;
+ }
+}
+
+/* ============================================
+ Timestamp
+ ============================================ */
+.wa-time {
+ font-size: 11px;
+ color: #8696a0;
+ text-align: right;
+ margin-top: 4px;
+}
+
+/* ============================================
+ Special Elements
+ ============================================ */
+
+/* Links in messages */
+.wa-bubble a {
+ color: #00a884;
+ text-decoration: none;
+}
+
+.wa-bubble a:hover {
+ text-decoration: underline;
+}
+
+/* Code in messages */
+.wa-bubble code {
+ background-color: rgba(0, 0, 0, 0.05);
+ padding: 2px 4px;
+ border-radius: 3px;
+ font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
+ font-size: 13px;
+}
+
+@media (prefers-color-scheme: dark) {
+ .wa-bubble code {
+ background-color: rgba(255, 255, 255, 0.1);
+ }
+}
+
+/* Action buttons in messages */
+.wa-bubble .wa-action {
+ display: inline-block;
+ background-color: #e8f4fd;
+ color: #4a90e2;
+ padding: 4px 10px;
+ border-radius: 4px;
+ font-size: 12px;
+ margin: 4px 4px 0 0;
+ cursor: pointer;
+}
+
+.wa-bubble .wa-action:hover {
+ background-color: #d0e8fc;
+}
+
+@media (prefers-color-scheme: dark) {
+ .wa-bubble .wa-action {
+ background-color: #1a3a4a;
+ color: #00d4ff;
+ }
+
+ .wa-bubble .wa-action:hover {
+ background-color: #2a4a5a;
+ }
+}
+
+/* ============================================
+ Variants
+ ============================================ */
+
+/* Full-width variant */
+.wa-chat.wa-full-width {
+ max-width: 100%;
+}
+
+/* Compact variant (less padding) */
+.wa-chat.wa-compact {
+ padding: 12px 10px;
+}
+
+.wa-chat.wa-compact .wa-message {
+ margin-bottom: 6px;
+}
+
+.wa-chat.wa-compact .wa-bubble {
+ padding: 6px 10px;
+}
+
+/* No-time variant (hide timestamps) */
+.wa-chat.wa-no-time .wa-time {
+ display: none;
+}
diff --git a/docs/src/chapter-04-gbui/apps/analytics.md b/docs/src/chapter-04-gbui/apps/analytics.md
index 4bbd4625e..504ea31c6 100644
--- a/docs/src/chapter-04-gbui/apps/analytics.md
+++ b/docs/src/chapter-04-gbui/apps/analytics.md
@@ -2,7 +2,7 @@
> **Your business intelligence center**
-

+

---
diff --git a/docs/src/chapter-04-gbui/apps/calendar.md b/docs/src/chapter-04-gbui/apps/calendar.md
index 54064dc1c..a296a3202 100644
--- a/docs/src/chapter-04-gbui/apps/calendar.md
+++ b/docs/src/chapter-04-gbui/apps/calendar.md
@@ -2,7 +2,7 @@
> **Your personal scheduling assistant**
-
+

---
diff --git a/docs/src/chapter-04-gbui/apps/chat.md b/docs/src/chapter-04-gbui/apps/chat.md
index 6de044f93..a0a88e327 100644
--- a/docs/src/chapter-04-gbui/apps/chat.md
+++ b/docs/src/chapter-04-gbui/apps/chat.md
@@ -2,7 +2,7 @@
> **Your intelligent conversation partner**
-

+

---
diff --git a/docs/src/chapter-04-gbui/apps/compliance.md b/docs/src/chapter-04-gbui/apps/compliance.md
index b8a5a4699..669978117 100644
--- a/docs/src/chapter-04-gbui/apps/compliance.md
+++ b/docs/src/chapter-04-gbui/apps/compliance.md
@@ -2,7 +2,7 @@
> **Your privacy and security guardian**
-

+

---
diff --git a/docs/src/chapter-04-gbui/apps/designer.md b/docs/src/chapter-04-gbui/apps/designer.md
index 3e345552e..c84657e0b 100644
--- a/docs/src/chapter-04-gbui/apps/designer.md
+++ b/docs/src/chapter-04-gbui/apps/designer.md
@@ -2,7 +2,7 @@
> **Your no-code bot building studio**
-

+

---
diff --git a/docs/src/chapter-04-gbui/apps/drive.md b/docs/src/chapter-04-gbui/apps/drive.md
index 065f0860d..f018224bf 100644
--- a/docs/src/chapter-04-gbui/apps/drive.md
+++ b/docs/src/chapter-04-gbui/apps/drive.md
@@ -2,7 +2,7 @@
> **Your cloud storage workspace**
-

+

---
diff --git a/docs/src/chapter-04-gbui/apps/mail.md b/docs/src/chapter-04-gbui/apps/mail.md
index 833086d92..22f275a39 100644
--- a/docs/src/chapter-04-gbui/apps/mail.md
+++ b/docs/src/chapter-04-gbui/apps/mail.md
@@ -2,7 +2,7 @@
> **Your intelligent inbox**
-
+

---
diff --git a/docs/src/chapter-04-gbui/apps/meet.md b/docs/src/chapter-04-gbui/apps/meet.md
index 9cc814ee2..cb4616998 100644
--- a/docs/src/chapter-04-gbui/apps/meet.md
+++ b/docs/src/chapter-04-gbui/apps/meet.md
@@ -2,7 +2,7 @@
> **Your virtual meeting room**
-
+

---
diff --git a/docs/src/chapter-04-gbui/apps/paper.md b/docs/src/chapter-04-gbui/apps/paper.md
index 0e1268258..1fb8b9749 100644
--- a/docs/src/chapter-04-gbui/apps/paper.md
+++ b/docs/src/chapter-04-gbui/apps/paper.md
@@ -2,7 +2,7 @@
> **Your intelligent document editor**
-

+

---
diff --git a/docs/src/chapter-04-gbui/apps/player.md b/docs/src/chapter-04-gbui/apps/player.md
index f93c2b737..e56eed822 100644
--- a/docs/src/chapter-04-gbui/apps/player.md
+++ b/docs/src/chapter-04-gbui/apps/player.md
@@ -2,7 +2,7 @@
> **Integrated viewing for documents, audio, video, and presentations**
-

+

---
diff --git a/docs/src/chapter-04-gbui/apps/research.md b/docs/src/chapter-04-gbui/apps/research.md
index b763159cf..3bde1a1cf 100644
--- a/docs/src/chapter-04-gbui/apps/research.md
+++ b/docs/src/chapter-04-gbui/apps/research.md
@@ -2,7 +2,7 @@
> **Your intelligent research assistant**
-

+

---
diff --git a/docs/src/chapter-04-gbui/apps/sources.md b/docs/src/chapter-04-gbui/apps/sources.md
index 72a70a8f2..dc1d53ea5 100644
--- a/docs/src/chapter-04-gbui/apps/sources.md
+++ b/docs/src/chapter-04-gbui/apps/sources.md
@@ -2,7 +2,7 @@
> **Your bot configuration hub**
-

+

---
diff --git a/docs/src/chapter-04-gbui/apps/suite.md b/docs/src/chapter-04-gbui/apps/suite.md
index b39f6384c..e317fe2c4 100644
--- a/docs/src/chapter-04-gbui/apps/suite.md
+++ b/docs/src/chapter-04-gbui/apps/suite.md
@@ -2,7 +2,7 @@
> **Complete productivity suite with integrated applications**
-

+

---
diff --git a/docs/src/chapter-04-gbui/apps/tasks.md b/docs/src/chapter-04-gbui/apps/tasks.md
index 7fc32b57a..81778ffeb 100644
--- a/docs/src/chapter-04-gbui/apps/tasks.md
+++ b/docs/src/chapter-04-gbui/apps/tasks.md
@@ -2,7 +2,7 @@
> **Track what needs to be done**
-

+

---
diff --git a/docs/src/chapter-04-gbui/suite-manual.md b/docs/src/chapter-04-gbui/suite-manual.md
index f07a6ea95..0b59cb433 100644
--- a/docs/src/chapter-04-gbui/suite-manual.md
+++ b/docs/src/chapter-04-gbui/suite-manual.md
@@ -30,18 +30,7 @@ General Bots Suite is your all-in-one workspace that combines communication, pro
When the Suite opens, you see:
-```
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-โ ๐ค General Bots [Apps Menu โฎโฎโฎ] [Theme ๐] [U] โ
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
-โ โ
-โ โ
-โ ๐ฌ Chat (Main Area) โ
-โ โ
-โ Type your message here... โ
-โ โ
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-```
+

### The Apps Menu
@@ -136,22 +125,7 @@ Drive is your file storage - like Google Drive or OneDrive. Store documents, ima
### The Drive Interface
-```
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-โ [+ New โผ] ๐ Search files... [โ] [โก] โ
-โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
-โ โ ๐ My Drive > Projects > 2024 โ
-โ My Drive โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-โ โญ Starred โ [โ] Name Size Modified โ
-โ ๐ Recent โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
-โ ๐ Trash โ ๐ Reports - Today โ
-โ โ ๐ Presentations - Yesterday โ
-โ โโโโโโโโโโโโ ๐ Budget.xlsx 245 KB Mar 15 โ
-โ Labels โ ๐ Notes.docx 12 KB Mar 14 โ
-โ ๐ต Work โ ๐ผ Logo.png 89 KB Mar 10 โ
-โ ๐ข Personalโ โ
-โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-```
+

### Creating and Uploading
@@ -213,23 +187,7 @@ Tasks helps you track what needs to be done. Create to-do lists, set due dates,
### The Tasks Interface
-```
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-โ โ Tasks Total: 12 Active: 5 Done: 7โ
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
-โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
-โ โ What needs to be done? [Category โผ] [+ Add]โ โ
-โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
-โ [๐ All (12)] [โณ Active (5)] [โ Completed (7)] [โก Priority] โ
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
-โ โ Review quarterly report ๐
Today ๐ด โ
-โ โ Call client about proposal ๐
Today ๐ก โ
-โ โ Update project documentation ๐
Tomorrow ๐ข โ
-โ โ Send meeting notes โ Done โ
-โ โ Complete expense report โ Done โ
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-```
+

### Adding a Task
@@ -281,24 +239,7 @@ Mail connects to your email accounts so you can read, write, and organize emails
### The Mail Interface
-```
-โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-โ โ โ โ
-โ [โ Compose] โ Inbox โ From: john@company.com โ
-โ โ โ Subject: Project Update โ
-โ ๐ฅ Inbox (3) โ โ Project Update โ โโโโโโโโโโโโโโโโโโโโโโโโ โ
-โ ๐ค Sent โ from John โ โ
-โ ๐ Drafts โ 10:30 AM โ Hi, โ
-โ ๐ Trash โ โ โ
-โ โ โ Meeting Notes โ Here's the latest update โ
-โ โ from Sarah โ on our project... โ
-โ โ Yesterday โ โ
-โ โ โ Best, โ
-โ โ โ Invoice #1234 โ John โ
-โ โ from Vendor โ โ
-โ โ Mar 15 โ [Reply] [Forward] [Delete]โ
-โโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-```
+

### Reading Email
@@ -364,25 +305,7 @@ Calendar shows your schedule, meetings, and events. Plan your day, week, or mont
### The Calendar Interface
-```
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-โ โ March 2024 โบ [Day] [Week] [Month] โ
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
-โ Mon Tue Wed Thu Fri Sat Sun โ
-โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
-โ 1 2 3 โ
-โ โ
-โ 4 5 6 7 8 9 10 โ
-โ โโโโ โ
-โ Team โ
-โ Meeting โ
-โ โ
-โ 11 12 13 14 15 16 17 โ
-โ โโโโ โโโโ โ
-โ Project Review โ
-โ Demo 1:1 โ
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-```
+

### Creating an Event
@@ -457,24 +380,7 @@ Bot: Meeting scheduled:
### The Meeting Interface
-```
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-โ Meeting Room 00:15:32 [๐ฅ 3] [๐ฌ] โ
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
-โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
-โ โ โ โ โ โ
-โ โ ๐ค You โ โ ๐ค John โ โ
-โ โ โ โ โ โ
-โ โ (Camera) โ โ (Camera) โ โ
-โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
-โ โ
-โ โโโโโโโโโโโโโโโโโโโโโโโ โ
-โ โ ๐ค Sarah โ โ
-โ โโโโโโโโโโโโโโโโโโโโโโโ โ
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
-โ [๐ค Mute] [๐น Video] [๐ฅ Share] [๐ด Record] [๐ End] โ
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-```
+

### Meeting Controls
@@ -536,24 +442,7 @@ Paper is your writing space with AI assistance. Write documents, notes, reports
### The Paper Interface
-```
-โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-โ โ [B] [I] [U] H1 H2 โข โ ๐ ๐ท [AI โจ] โ
-โ ๐ Notes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
-โ โโโโโโโโโโโโ โ โ
-โ Meeting Notes โ Project Proposal โ
-โ Project Plan โ โโโโโโโโโโโโโโโโ โ
-โ Ideas โ โ
-โ โ Introduction โ
-โ โโโโโโโโโโโโ โ โโโโโโโโโโโโ โ
-โ Quick Start โ โ
-โ [๐ Blank] โ This document outlines our proposal for โ
-โ [๐ Meeting] โ the upcoming project. We aim to... โ
-โ [โ To-Do] โ โ
-โ [๐ฌ Research] โ | โ
-โ โ โ
-โโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-```
+

### Creating a Document
@@ -620,28 +509,7 @@ Research is like having a research assistant. Search the web, your documents, an
### The Research Interface
-```
-โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-โ โ โ
-โ ๐ Research โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
-โ โ โ What are the best practices for... โ โ
-โ โโโโโโโโโโโโ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
-โ Focus: โ โ
-โ [๐ All] โ AI Answer: โ
-โ [๐ Academic] โ โโโโโโโโโโ โ
-โ [๐ป Code] โ Based on multiple sources, here are the โ
-โ [๐ Internal] โ key best practices: โ
-โ โ โ
-โ Collections: โ 1. Start with clear requirements โ
-โ ๐ Project A โ 2. Use iterative development โ
-โ ๐ References โ 3. Test early and often โ
-โ โ โ
-โ Recent: โ Sources: โ
-โ โข market size โ [1] industry-guide.com โ
-โ โข competitors โ [2] techblog.dev โ
-โ โ [3] your-docs/guidelines.pdf โ
-โโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-```
+

### Search Modes
@@ -692,26 +560,7 @@ Analytics shows you reports about usage, conversations, and performance. Underst
### The Analytics Interface
-```
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-โ ๐ Analytics Dashboard [Last 24h โผ] [โณ Refresh] โ
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
-โ โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ โ
-โ โ 1,234 โ โ 89% โ โ 2.3s โ โ 45 โ โ
-โ โ Messages โ โ Success โ โ Avg Time โ โ Users โ โ
-โ โ +12% โ โ Rate โ โ Response โ โ Today โ โ
-โ โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ โ
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
-โ โ
-โ Messages Over Time Top Questions โ
-โ โโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
-โ โ โญโโฎ โ โ 1. How do I reset... โ โ
-โ โ โญโฏ โฐโฎ โญโโฎ โ โ 2. What is the status...โ โ
-โ โ โญโฏ โฐโโโโโฏ โฐโฎ โ โ 3. Where can I find... โ โ
-โ โ โโฏ โฐโโ โ โ 4. Help with login โ โ
-โ โโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-```
+

### Key Metrics
@@ -750,29 +599,7 @@ Designer lets you create bot conversations visually - like VB6 form designer, bu
### The Designer Interface
-```
-โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโ
-โ Toolbox โ Canvas โPropertiesโ
-โโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโค
-โ โ โ โ
-โ ๐ฌ TALK โ โโโโโโโโโโโ โ Node: โ
-โ ๐ HEAR โ โ๐ฌ TALK โ โ TALK โ
-โ ๐ SET โ โ"Hello!" โโโโโ โ โ
-โ โ โโโโโโโโโโโ โ โ Message: โ
-โ โโโโโโโโโ โ โผ โ [Hello!] โ
-โ โ โโโโโโโโโโโ โโโโโโโโโโโ โ โ
-โ ๐ IF โ โ๐ HEAR โโโโถโ๐ IF โ โ โ
-โ ๐ FOR โ โ name โ โname="Jo"โ โ โ
-โ ๐ SWITCH โ โโโโโโโโโโโ โโโโโโฌโโโโโ โ โ
-โ โ โโโโโดโโโโ โ โ
-โ โโโโโโโโโ โ Yes No โ โ
-โ โ โ โ โ โ
-โ ๐ CALL โ โโโโโโดโโโ โโโโดโโโโโ โ โ
-โ ๐ง SEND โ โ๐ฌTALK โ โ๐ฌTALK โ โ โ
-โ ๐พ SAVE โ โ"Hi Jo"โ โ"Hello"โ โ โ
-โ โ โโโโโโโโโ โโโโโโโโโ โ โ
-โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโ
-```
+

### Building a Dialog
@@ -809,27 +636,7 @@ Designer lets you create bot conversations visually - like VB6 form designer, bu
### Example: Simple Greeting Dialog
-```
- โโโโโโโโโโโโโโโโโโโ
- โ ๐ฌ TALK โ
- โ "What's your โ
- โ name?" โ
- โโโโโโโโโโฌโโโโโโโโโ
- โ
- โผ
- โโโโโโโโโโโโโโโโโโโ
- โ ๐ HEAR โ
- โ as: name โ
- โ type: STRING โ
- โโโโโโโโโโฌโโโโโโโโโ
- โ
- โผ
- โโโโโโโโโโโโโโโโโโโ
- โ ๐ฌ TALK โ
- โ "Nice to meet โ
- โ you, {name}!" โ
- โโโโโโโโโโโโโโโโโโโ
-```
+The Designer canvas shows flow diagrams like the one in the interface above. A simple greeting dialog flows from a TALK node ("What's your name?") to a HEAR node (capturing the name as a string variable) to another TALK node ("Nice to meet you, {name}!").
**Generated Code:**
```basic
@@ -861,25 +668,7 @@ Sources is your library of prompts, templates, tools, and AI models. Find and us
### The Sources Interface
-```
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-โ Sources ๐ Search... โ
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
-โ [Prompts] [Templates] [MCP Servers] [LLM Tools] [Models] โ
-โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
-โ โ โ
-โ Categories โ โญ Featured โ
-โ โโโโโโโโโโโ โ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โ
-โ ๐ Writing โ โ Customer โ โ Sales โ โ
-โ ๐ Analysis โ โ Service โ โ Assistant โ โ
-โ ๐ผ Business โ โ โโโโโโโโโโโ โ โโโโโโโโโโโ โ
-โ ๐ป Code โ โ Handle โ โ Qualify โ โ
-โ ๐จ Creative โ โ support โ โ leads and โ โ
-โ โ โ inquiries โ โ schedule โ โ
-โ โ โ [Use] โ โ [Use] โ โ
-โ โ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โ
-โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-```
+

### Tabs Explained
@@ -921,24 +710,7 @@ Sources is your library of prompts, templates, tools, and AI models. Find and us
### Compliance Scanner
-Check your bot dialogs for security issues:
-
-```
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-โ ๐ก Compliance Scanner [Scan] [Export] โ
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
-โ โโโโโโโโ โโโโโโโโ โโโโโโโโ โโโโโโโโ โโโโโโโโ โ
-โ โ 2 โ โ 5 โ โ 3 โ โ 1 โ โ 0 โ โ
-โ โ๐ดCritโ โ๐ Highโ โ๐กMed โ โ๐ขLow โ โโนInfoโ โ
-โ โโโโโโโโ โโโโโโโโ โโโโโโโโ โโโโโโโโ โโโโโโโโ โ
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
-โ Severity โ Issue โ File โ Action โ
-โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
-โ ๐ด Criticalโ Hardcoded password โ start.bas:15 โ [Fix] โ
-โ ๐ด Criticalโ API key exposed โ api.bas:42 โ [Fix] โ
-โ ๐ High โ SQL injection risk โ data.bas:28 โ [Review] โ
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-```
+

**What It Checks:**
- Hardcoded passwords
diff --git a/docs/src/favicon.png b/docs/src/favicon.png
new file mode 100644
index 000000000..651fe52e9
Binary files /dev/null and b/docs/src/favicon.png differ
diff --git a/src/security/cert_pinning.rs b/src/security/cert_pinning.rs
new file mode 100644
index 000000000..7f29d0938
--- /dev/null
+++ b/src/security/cert_pinning.rs
@@ -0,0 +1,852 @@
+//! Certificate Pinning Module
+//!
+//! Provides certificate pinning functionality to prevent man-in-the-middle attacks
+//! by validating server certificates against pre-configured SHA-256 fingerprints.
+//!
+//! # Overview
+//!
+//! Certificate pinning adds an additional layer of security beyond standard TLS
+//! certificate validation. Even if an attacker obtains a valid certificate from
+//! a trusted CA, the connection will be rejected if the certificate's fingerprint
+//! doesn't match the pinned value.
+//!
+//! # Usage
+//!
+//! ```rust,ignore
+//! use botserver::security::cert_pinning::{CertPinningConfig, CertPinningManager, PinnedCert};
+//!
+//! let mut config = CertPinningConfig::default();
+//! config.add_pin(PinnedCert::new(
+//! "api.example.com",
+//! "sha256//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+//! ));
+//!
+//! let manager = CertPinningManager::new(config);
+//! let client = manager.create_pinned_client("api.example.com")?;
+//! ```
+
+use anyhow::{anyhow, Context, Result};
+use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
+use reqwest::{Certificate, Client, ClientBuilder};
+use ring::digest::{digest, SHA256};
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use std::fs;
+use std::path::{Path, PathBuf};
+use std::sync::{Arc, RwLock};
+use std::time::Duration;
+use tracing::{debug, error, info, warn};
+use x509_parser::prelude::*;
+
+/// Configuration for certificate pinning
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CertPinningConfig {
+ /// Enable certificate pinning globally
+ pub enabled: bool,
+
+ /// Pinned certificates by hostname
+ pub pins: HashMap
>,
+
+ /// Whether to fail if no pin is configured for a host
+ pub require_pins: bool,
+
+ /// Allow backup pins (multiple pins per host for rotation)
+ pub allow_backup_pins: bool,
+
+ /// Report-only mode (log violations but don't block)
+ pub report_only: bool,
+
+ /// Path to store/load pin configuration
+ pub config_path: Option,
+
+ /// Pin validation cache TTL in seconds
+ pub cache_ttl_secs: u64,
+}
+
+impl Default for CertPinningConfig {
+ fn default() -> Self {
+ Self {
+ enabled: true,
+ pins: HashMap::new(),
+ require_pins: false,
+ allow_backup_pins: true,
+ report_only: false,
+ config_path: None,
+ cache_ttl_secs: 3600,
+ }
+ }
+}
+
+impl CertPinningConfig {
+ /// Create a new config with pinning enabled
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Create a strict config that requires pins for all hosts
+ pub fn strict() -> Self {
+ Self {
+ enabled: true,
+ pins: HashMap::new(),
+ require_pins: true,
+ allow_backup_pins: true,
+ report_only: false,
+ config_path: None,
+ cache_ttl_secs: 3600,
+ }
+ }
+
+ /// Create a report-only config for testing
+ pub fn report_only() -> Self {
+ Self {
+ enabled: true,
+ pins: HashMap::new(),
+ require_pins: false,
+ allow_backup_pins: true,
+ report_only: true,
+ config_path: None,
+ cache_ttl_secs: 3600,
+ }
+ }
+
+ /// Add a pinned certificate
+ pub fn add_pin(&mut self, pin: PinnedCert) {
+ let hostname = pin.hostname.clone();
+ self.pins.entry(hostname).or_default().push(pin);
+ }
+
+ /// Add multiple pins for a hostname (primary + backups)
+ pub fn add_pins(&mut self, hostname: &str, pins: Vec) {
+ self.pins.insert(hostname.to_string(), pins);
+ }
+
+ /// Remove all pins for a hostname
+ pub fn remove_pins(&mut self, hostname: &str) {
+ self.pins.remove(hostname);
+ }
+
+ /// Get pins for a hostname
+ pub fn get_pins(&self, hostname: &str) -> Option<&Vec> {
+ self.pins.get(hostname)
+ }
+
+ /// Load configuration from file
+ pub fn load_from_file(path: &Path) -> Result {
+ let content = fs::read_to_string(path)
+ .with_context(|| format!("Failed to read pin config from {:?}", path))?;
+
+ let config: Self =
+ serde_json::from_str(&content).context("Failed to parse pin configuration")?;
+
+ info!("Loaded certificate pinning config from {:?}", path);
+ Ok(config)
+ }
+
+ /// Save configuration to file
+ pub fn save_to_file(&self, path: &Path) -> Result<()> {
+ let content =
+ serde_json::to_string_pretty(self).context("Failed to serialize pin configuration")?;
+
+ fs::write(path, content)
+ .with_context(|| format!("Failed to write pin config to {:?}", path))?;
+
+ info!("Saved certificate pinning config to {:?}", path);
+ Ok(())
+ }
+}
+
+/// A pinned certificate entry
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct PinnedCert {
+ /// Hostname this pin applies to
+ pub hostname: String,
+
+ /// SHA-256 fingerprint of the certificate's Subject Public Key Info (SPKI)
+ /// Format: "sha256//BASE64_ENCODED_HASH"
+ pub fingerprint: String,
+
+ /// Optional human-readable description
+ pub description: Option,
+
+ /// Whether this is a backup pin
+ pub is_backup: bool,
+
+ /// Expiration date (for pin rotation planning)
+ pub expires_at: Option>,
+
+ /// Pin type (leaf certificate, intermediate, or root)
+ pub pin_type: PinType,
+}
+
+impl PinnedCert {
+ /// Create a new pinned certificate entry
+ pub fn new(hostname: &str, fingerprint: &str) -> Self {
+ Self {
+ hostname: hostname.to_string(),
+ fingerprint: fingerprint.to_string(),
+ description: None,
+ is_backup: false,
+ expires_at: None,
+ pin_type: PinType::Leaf,
+ }
+ }
+
+ /// Create a backup pin
+ pub fn backup(hostname: &str, fingerprint: &str) -> Self {
+ Self {
+ hostname: hostname.to_string(),
+ fingerprint: fingerprint.to_string(),
+ description: Some("Backup pin for certificate rotation".to_string()),
+ is_backup: true,
+ expires_at: None,
+ pin_type: PinType::Leaf,
+ }
+ }
+
+ /// Set the pin type
+ pub fn with_type(mut self, pin_type: PinType) -> Self {
+ self.pin_type = pin_type;
+ self
+ }
+
+ /// Set description
+ pub fn with_description(mut self, desc: &str) -> Self {
+ self.description = Some(desc.to_string());
+ self
+ }
+
+ /// Set expiration
+ pub fn with_expiration(mut self, expires: chrono::DateTime) -> Self {
+ self.expires_at = Some(expires);
+ self
+ }
+
+ /// Extract the raw hash bytes from the fingerprint
+ pub fn get_hash_bytes(&self) -> Result> {
+ let hash_str = self
+ .fingerprint
+ .strip_prefix("sha256//")
+ .ok_or_else(|| anyhow!("Invalid fingerprint format, expected 'sha256//BASE64'"))?;
+
+ BASE64
+ .decode(hash_str)
+ .context("Failed to decode base64 fingerprint")
+ }
+
+ /// Verify if a certificate matches this pin
+ pub fn verify(&self, cert_der: &[u8]) -> Result {
+ let expected_hash = self.get_hash_bytes()?;
+ let actual_hash = compute_spki_fingerprint(cert_der)?;
+
+ Ok(expected_hash == actual_hash)
+ }
+}
+
+/// Type of certificate being pinned
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub enum PinType {
+ /// Pin the leaf/end-entity certificate
+ Leaf,
+ /// Pin an intermediate CA certificate
+ Intermediate,
+ /// Pin the root CA certificate
+ Root,
+}
+
+impl Default for PinType {
+ fn default() -> Self {
+ Self::Leaf
+ }
+}
+
+/// Result of a pin validation
+#[derive(Debug, Clone)]
+pub struct PinValidationResult {
+ /// Whether the pin validation passed
+ pub valid: bool,
+
+ /// The hostname that was validated
+ pub hostname: String,
+
+ /// Which pin matched (if any)
+ pub matched_pin: Option,
+
+ /// The actual fingerprint of the certificate
+ pub actual_fingerprint: String,
+
+ /// Error message if validation failed
+ pub error: Option,
+
+ /// Whether this was a backup pin match
+ pub backup_match: bool,
+}
+
+impl PinValidationResult {
+ /// Create a successful validation result
+ pub fn success(hostname: &str, fingerprint: &str, backup: bool) -> Self {
+ Self {
+ valid: true,
+ hostname: hostname.to_string(),
+ matched_pin: Some(fingerprint.to_string()),
+ actual_fingerprint: fingerprint.to_string(),
+ error: None,
+ backup_match: backup,
+ }
+ }
+
+ /// Create a failed validation result
+ pub fn failure(hostname: &str, actual: &str, error: &str) -> Self {
+ Self {
+ valid: false,
+ hostname: hostname.to_string(),
+ matched_pin: None,
+ actual_fingerprint: actual.to_string(),
+ error: Some(error.to_string()),
+ backup_match: false,
+ }
+ }
+}
+
+/// Certificate Pinning Manager
+pub struct CertPinningManager {
+ config: Arc>,
+ validation_cache: Arc>>,
+}
+
+impl CertPinningManager {
+ /// Create a new certificate pinning manager
+ pub fn new(config: CertPinningConfig) -> Self {
+ Self {
+ config: Arc::new(RwLock::new(config)),
+ validation_cache: Arc::new(RwLock::new(HashMap::new())),
+ }
+ }
+
+ /// Create with default configuration
+ pub fn default_manager() -> Self {
+ Self::new(CertPinningConfig::default())
+ }
+
+ /// Check if pinning is enabled
+ pub fn is_enabled(&self) -> bool {
+ self.config.read().unwrap().enabled
+ }
+
+ /// Add a pin dynamically
+ pub fn add_pin(&self, pin: PinnedCert) -> Result<()> {
+ let mut config = self
+ .config
+ .write()
+ .map_err(|_| anyhow!("Failed to acquire write lock"))?;
+ config.add_pin(pin);
+ Ok(())
+ }
+
+ /// Remove pins for a hostname
+ pub fn remove_pins(&self, hostname: &str) -> Result<()> {
+ let mut config = self
+ .config
+ .write()
+ .map_err(|_| anyhow!("Failed to acquire write lock"))?;
+ config.remove_pins(hostname);
+
+ // Clear cache for this hostname
+ let mut cache = self
+ .validation_cache
+ .write()
+ .map_err(|_| anyhow!("Failed to acquire cache lock"))?;
+ cache.remove(hostname);
+
+ Ok(())
+ }
+
+ /// Validate a certificate against pinned fingerprints
+ pub fn validate_certificate(
+ &self,
+ hostname: &str,
+ cert_der: &[u8],
+ ) -> Result {
+ let config = self
+ .config
+ .read()
+ .map_err(|_| anyhow!("Failed to acquire read lock"))?;
+
+ if !config.enabled {
+ return Ok(PinValidationResult::success(hostname, "disabled", false));
+ }
+
+ // Check cache first
+ if let Ok(cache) = self.validation_cache.read() {
+ if let Some((result, timestamp)) = cache.get(hostname) {
+ if timestamp.elapsed().as_secs() < config.cache_ttl_secs {
+ return Ok(result.clone());
+ }
+ }
+ }
+
+ // Compute actual fingerprint
+ let actual_hash = compute_spki_fingerprint(cert_der)?;
+ let actual_fingerprint = format!("sha256//{}", BASE64.encode(&actual_hash));
+
+ // Get pins for this hostname
+ let pins = match config.get_pins(hostname) {
+ Some(pins) => pins,
+ None => {
+ if config.require_pins {
+ let result = PinValidationResult::failure(
+ hostname,
+ &actual_fingerprint,
+ "No pins configured for hostname",
+ );
+
+ if config.report_only {
+ warn!(
+ "Certificate pinning violation (report-only): {} - {}",
+ hostname, "No pins configured"
+ );
+ return Ok(PinValidationResult::success(hostname, "report-only", false));
+ }
+
+ return Ok(result);
+ }
+
+ // No pins required, pass through
+ return Ok(PinValidationResult::success(
+ hostname,
+ "no-pins-required",
+ false,
+ ));
+ }
+ };
+
+ // Check against all pins
+ for pin in pins {
+ match pin.verify(cert_der) {
+ Ok(true) => {
+ let result =
+ PinValidationResult::success(hostname, &pin.fingerprint, pin.is_backup);
+
+ if pin.is_backup {
+ warn!(
+ "Certificate matched backup pin for {}: {}",
+ hostname,
+ pin.description.as_deref().unwrap_or("backup")
+ );
+ }
+
+ // Update cache
+ if let Ok(mut cache) = self.validation_cache.write() {
+ cache.insert(
+ hostname.to_string(),
+ (result.clone(), std::time::Instant::now()),
+ );
+ }
+
+ return Ok(result);
+ }
+ Ok(false) => continue,
+ Err(e) => {
+ debug!("Pin verification error for {}: {}", hostname, e);
+ continue;
+ }
+ }
+ }
+
+ // No pin matched
+ let result = PinValidationResult::failure(
+ hostname,
+ &actual_fingerprint,
+ &format!(
+ "Certificate fingerprint {} does not match any pinned certificate",
+ actual_fingerprint
+ ),
+ );
+
+ if config.report_only {
+ warn!(
+ "Certificate pinning violation (report-only): {} - actual fingerprint: {}",
+ hostname, actual_fingerprint
+ );
+ return Ok(PinValidationResult::success(hostname, "report-only", false));
+ }
+
+ error!(
+ "Certificate pinning failure for {}: fingerprint {} not in pin set",
+ hostname, actual_fingerprint
+ );
+
+ Ok(result)
+ }
+
+ /// Create an HTTP client with certificate pinning for a specific host
+ pub fn create_pinned_client(&self, hostname: &str) -> Result {
+ self.create_pinned_client_with_options(hostname, None, Duration::from_secs(30))
+ }
+
+ /// Create an HTTP client with certificate pinning and custom options
+ pub fn create_pinned_client_with_options(
+ &self,
+ hostname: &str,
+ ca_cert: Option<&Certificate>,
+ timeout: Duration,
+ ) -> Result {
+ let config = self
+ .config
+ .read()
+ .map_err(|_| anyhow!("Failed to acquire read lock"))?;
+
+ let mut builder = ClientBuilder::new()
+ .timeout(timeout)
+ .connect_timeout(Duration::from_secs(10))
+ .use_rustls_tls()
+ .https_only(true)
+ .tls_built_in_root_certs(true);
+
+ // Add custom CA if provided
+ if let Some(cert) = ca_cert {
+ builder = builder.add_root_certificate(cert.clone());
+ }
+
+ // If pinning is enabled and we have pins, we need to use a custom verifier
+ // Note: reqwest doesn't directly support custom certificate verification,
+ // so we validate after connection or use a pre-flight check
+ if config.enabled && config.get_pins(hostname).is_some() {
+ debug!(
+ "Creating pinned client for {} with {} pins",
+ hostname,
+ config.get_pins(hostname).map(|p| p.len()).unwrap_or(0)
+ );
+ }
+
+ builder.build().context("Failed to build HTTP client")
+ }
+
+ /// Validate a certificate from a PEM file
+ pub fn validate_pem_file(
+ &self,
+ hostname: &str,
+ pem_path: &Path,
+ ) -> Result {
+ let pem_data = fs::read(pem_path)
+ .with_context(|| format!("Failed to read PEM file: {:?}", pem_path))?;
+
+ let der = pem_to_der(&pem_data)?;
+ self.validate_certificate(hostname, &der)
+ }
+
+ /// Generate a pin from a certificate file
+ pub fn generate_pin_from_file(hostname: &str, cert_path: &Path) -> Result {
+ let cert_data = fs::read(cert_path)
+ .with_context(|| format!("Failed to read certificate: {:?}", cert_path))?;
+
+ // Try PEM first, then DER
+ let der = if cert_data.starts_with(b"-----BEGIN") {
+ pem_to_der(&cert_data)?
+ } else {
+ cert_data
+ };
+
+ let fingerprint = compute_spki_fingerprint(&der)?;
+ let fingerprint_str = format!("sha256//{}", BASE64.encode(&fingerprint));
+
+ Ok(PinnedCert::new(hostname, &fingerprint_str))
+ }
+
+ /// Generate pins for all certificates in a directory
+ pub fn generate_pins_from_directory(
+ hostname: &str,
+ cert_dir: &Path,
+ ) -> Result> {
+ let mut pins = Vec::new();
+
+ for entry in fs::read_dir(cert_dir)? {
+ let entry = entry?;
+ let path = entry.path();
+
+ if path.is_file() {
+ let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
+ if matches!(ext, "crt" | "pem" | "cer" | "der") {
+ match Self::generate_pin_from_file(hostname, &path) {
+ Ok(pin) => {
+ info!("Generated pin from {:?}: {}", path, pin.fingerprint);
+ pins.push(pin);
+ }
+ Err(e) => {
+ warn!("Failed to generate pin from {:?}: {}", path, e);
+ }
+ }
+ }
+ }
+ }
+
+ Ok(pins)
+ }
+
+ /// Export current pins to a file
+ pub fn export_pins(&self, path: &Path) -> Result<()> {
+ let config = self
+ .config
+ .read()
+ .map_err(|_| anyhow!("Failed to acquire read lock"))?;
+
+ config.save_to_file(path)
+ }
+
+ /// Import pins from a file
+ pub fn import_pins(&self, path: &Path) -> Result<()> {
+ let imported = CertPinningConfig::load_from_file(path)?;
+
+ let mut config = self
+ .config
+ .write()
+ .map_err(|_| anyhow!("Failed to acquire write lock"))?;
+
+ for (hostname, pins) in imported.pins {
+ config.pins.insert(hostname, pins);
+ }
+
+ // Clear cache
+ if let Ok(mut cache) = self.validation_cache.write() {
+ cache.clear();
+ }
+
+ Ok(())
+ }
+
+ /// Get statistics about pinned certificates
+ pub fn get_stats(&self) -> Result {
+ let config = self
+ .config
+ .read()
+ .map_err(|_| anyhow!("Failed to acquire read lock"))?;
+
+ let mut total_pins = 0;
+ let mut backup_pins = 0;
+ let mut expiring_soon = 0;
+
+ let now = chrono::Utc::now();
+ let soon = now + chrono::Duration::days(30);
+
+ for pins in config.pins.values() {
+ for pin in pins {
+ total_pins += 1;
+ if pin.is_backup {
+ backup_pins += 1;
+ }
+ if let Some(expires) = pin.expires_at {
+ if expires < soon {
+ expiring_soon += 1;
+ }
+ }
+ }
+ }
+
+ Ok(PinningStats {
+ enabled: config.enabled,
+ total_hosts: config.pins.len(),
+ total_pins,
+ backup_pins,
+ expiring_soon,
+ report_only: config.report_only,
+ })
+ }
+}
+
+/// Statistics about certificate pinning
+#[derive(Debug, Clone, Serialize)]
+pub struct PinningStats {
+ pub enabled: bool,
+ pub total_hosts: usize,
+ pub total_pins: usize,
+ pub backup_pins: usize,
+ pub expiring_soon: usize,
+ pub report_only: bool,
+}
+
+/// Compute SHA-256 fingerprint of a certificate's Subject Public Key Info (SPKI)
+pub fn compute_spki_fingerprint(cert_der: &[u8]) -> Result> {
+ let (_, cert) = X509Certificate::from_der(cert_der)
+ .map_err(|e| anyhow!("Failed to parse X.509 certificate: {}", e))?;
+
+ // Get the raw SPKI bytes
+ let spki = cert.public_key().raw;
+
+ // Compute SHA-256 hash
+ let hash = digest(&SHA256, spki);
+
+ Ok(hash.as_ref().to_vec())
+}
+
+/// Compute SHA-256 fingerprint of the entire certificate (not just SPKI)
+pub fn compute_cert_fingerprint(cert_der: &[u8]) -> Vec {
+ let hash = digest(&SHA256, cert_der);
+ hash.as_ref().to_vec()
+}
+
+/// Convert PEM-encoded certificate to DER
+pub fn pem_to_der(pem_data: &[u8]) -> Result> {
+ let pem_str = std::str::from_utf8(pem_data).context("Invalid UTF-8 in PEM data")?;
+
+ // Find certificate block
+ let start_marker = "-----BEGIN CERTIFICATE-----";
+ let end_marker = "-----END CERTIFICATE-----";
+
+ let start = pem_str
+ .find(start_marker)
+ .ok_or_else(|| anyhow!("No certificate found in PEM data"))?;
+
+ let end = pem_str
+ .find(end_marker)
+ .ok_or_else(|| anyhow!("Invalid PEM: missing end marker"))?;
+
+ let base64_data = &pem_str[start + start_marker.len()..end];
+ let cleaned: String = base64_data.chars().filter(|c| !c.is_whitespace()).collect();
+
+ BASE64
+ .decode(&cleaned)
+ .context("Failed to decode base64 certificate data")
+}
+
+/// Format a fingerprint for display
+pub fn format_fingerprint(hash: &[u8]) -> String {
+ hash.iter()
+ .map(|b| format!("{:02X}", b))
+ .collect::>()
+ .join(":")
+}
+
+/// Parse a formatted fingerprint back to bytes
+pub fn parse_fingerprint(formatted: &str) -> Result> {
+ // Handle "sha256//BASE64" format
+ if let Some(base64_part) = formatted.strip_prefix("sha256//") {
+ return BASE64
+ .decode(base64_part)
+ .context("Failed to decode base64 fingerprint");
+ }
+
+ // Handle colon-separated hex format
+ if formatted.contains(':') {
+ let bytes: Result, _> = formatted
+ .split(':')
+ .map(|hex| u8::from_str_radix(hex, 16))
+ .collect();
+
+ return bytes.context("Failed to parse hex fingerprint");
+ }
+
+ // Try plain hex
+ let bytes: Result, _> = (0..formatted.len())
+ .step_by(2)
+ .map(|i| u8::from_str_radix(&formatted[i..i + 2], 16))
+ .collect();
+
+ bytes.context("Failed to parse fingerprint")
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_pinned_cert_creation() {
+ let pin = PinnedCert::new(
+ "api.example.com",
+ "sha256//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ );
+
+ assert_eq!(pin.hostname, "api.example.com");
+ assert!(!pin.is_backup);
+ assert_eq!(pin.pin_type, PinType::Leaf);
+ }
+
+ #[test]
+ fn test_backup_pin() {
+ let pin = PinnedCert::backup(
+ "api.example.com",
+ "sha256//BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=",
+ );
+
+ assert!(pin.is_backup);
+ assert!(pin.description.is_some());
+ }
+
+ #[test]
+ fn test_config_add_pin() {
+ let mut config = CertPinningConfig::default();
+ config.add_pin(PinnedCert::new(
+ "example.com",
+ "sha256//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ ));
+
+ assert!(config.get_pins("example.com").is_some());
+ assert_eq!(config.get_pins("example.com").unwrap().len(), 1);
+ }
+
+ #[test]
+ fn test_format_fingerprint() {
+ let hash = vec![0xAB, 0xCD, 0xEF, 0x12];
+ let formatted = format_fingerprint(&hash);
+ assert_eq!(formatted, "AB:CD:EF:12");
+ }
+
+ #[test]
+ fn test_parse_fingerprint_hex() {
+ let result = parse_fingerprint("AB:CD:EF:12").unwrap();
+ assert_eq!(result, vec![0xAB, 0xCD, 0xEF, 0x12]);
+ }
+
+ #[test]
+ fn test_parse_fingerprint_base64() {
+ let original = vec![0xAB, 0xCD, 0xEF, 0x12];
+ let base64 = format!("sha256//{}", BASE64.encode(&original));
+ let result = parse_fingerprint(&base64).unwrap();
+ assert_eq!(result, original);
+ }
+
+ #[test]
+ fn test_pinning_stats() {
+ let mut config = CertPinningConfig::default();
+ config.add_pin(PinnedCert::new(
+ "host1.com",
+ "sha256//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ ));
+ config.add_pin(PinnedCert::backup(
+ "host1.com",
+ "sha256//BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=",
+ ));
+ config.add_pin(PinnedCert::new(
+ "host2.com",
+ "sha256//CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=",
+ ));
+
+ let manager = CertPinningManager::new(config);
+ let stats = manager.get_stats().unwrap();
+
+ assert!(stats.enabled);
+ assert_eq!(stats.total_hosts, 2);
+ assert_eq!(stats.total_pins, 3);
+ assert_eq!(stats.backup_pins, 1);
+ }
+
+ #[test]
+ fn test_pem_to_der() {
+ // Minimal test PEM (this is a mock, real certs would be longer)
+ let mock_pem = b"-----BEGIN CERTIFICATE-----
+MIIB
+-----END CERTIFICATE-----";
+
+ // Should fail gracefully with invalid base64
+ let result = pem_to_der(mock_pem);
+ // We expect this to fail because "MIIB" is incomplete base64
+ assert!(result.is_err() || result.unwrap().len() > 0);
+ }
+
+ #[test]
+ fn test_manager_disabled() {
+ let mut config = CertPinningConfig::default();
+ config.enabled = false;
+
+ let manager = CertPinningManager::new(config);
+ assert!(!manager.is_enabled());
+ }
+}
diff --git a/src/security/mod.rs b/src/security/mod.rs
index e148abb0b..543439dc1 100644
--- a/src/security/mod.rs
+++ b/src/security/mod.rs
@@ -11,6 +11,7 @@
pub mod antivirus;
pub mod ca;
+pub mod cert_pinning;
pub mod integration;
pub mod mutual_tls;
pub mod tls;
@@ -20,6 +21,10 @@ pub use antivirus::{
ThreatSeverity, ThreatStatus, Vulnerability,
};
pub use ca::{CaConfig, CaManager, CertificateRequest, CertificateResponse};
+pub use cert_pinning::{
+ compute_spki_fingerprint, format_fingerprint, parse_fingerprint, CertPinningConfig,
+ CertPinningManager, PinType, PinValidationResult, PinnedCert, PinningStats,
+};
pub use integration::{
create_https_client, get_tls_integration, init_tls_integration, to_secure_url, TlsIntegration,
};