CodeIssuesPull RequestsActionsSecurityInsights
✨ AI
More
Settings

Complete website visual overhaul — bold, premium design #3987

Merged⚡ AI-generatedXSccantynz wants to mergeclaude/website-overhaul-v2mainopened Mar 29, 20260/4 tasks
62 changed files+5137−2213
Added.github/CODEOWNERS+26−0View fileUnifiedSplit
1# 48co Voice — Code Ownership
2# These owners are automatically requested for PR review.
3
4# Default — all files
5* @ccantynz-alt
6
7# Website (Next.js)
8/app/ @ccantynz-alt
9/components/ @ccantynz-alt
10/lib/ @ccantynz-alt
11
12# Desktop App (Tauri / Rust)
13/tauri-app/ @ccantynz-alt
14
15# Chrome Extension
16/extension/ @ccantynz-alt
17
18# Mobile
19/ios-keyboard/ @ccantynz-alt
20/android-keyboard/ @ccantynz-alt
21
22# Shared Rust Core
23/shared-rust/ @ccantynz-alt
24
25# CI/CD & Repo Config
26/.github/ @ccantynz-alt
Added.github/ISSUE_TEMPLATE/bug_report.md+26−0View fileUnifiedSplit
1---
2name: Bug Report
3about: Something isn't working correctly
4title: "[Bug] "
5labels: bug
6assignees: ''
7---
8
9**What happened?**
10<!-- A clear description of the bug. -->
11
12**What did you expect?**
13<!-- What should have happened instead. -->
14
15**Steps to reproduce**
161.
172.
183.
19
20**Platform**
21- OS: <!-- e.g. Windows 11, macOS 14, iOS 17 -->
22- App: <!-- e.g. Desktop app, Chrome extension, website -->
23- Browser (if applicable): <!-- e.g. Chrome 124 -->
24
25**Screenshots**
26<!-- Attach screenshots if applicable. -->
Added.github/ISSUE_TEMPLATE/feature_request.md+24−0View fileUnifiedSplit
1---
2name: Feature Request
3about: Suggest a new feature or improvement
4title: "[Feature] "
5labels: enhancement
6assignees: ''
7---
8
9**What problem does this solve?**
10<!-- Describe the problem or need. -->
11
12**What's the solution?**
13<!-- How should this work from a user's perspective? -->
14
15**Which platform?**
16- [ ] Desktop (Mac/Windows)
17- [ ] Chrome Extension
18- [ ] Website
19- [ ] iPhone/iPad
20- [ ] Android
21- [ ] All
22
23**Additional context**
24<!-- Any competitor examples, screenshots, or references. -->
Added.github/PULL_REQUEST_TEMPLATE.md+21−0View fileUnifiedSplit
1## Summary
2
3<!-- What does this PR do? 1-2 sentences. -->
4
5## Changes
6
7<!-- Bullet list of what changed and why. -->
8
9-
10
11## Testing
12
13<!-- How was this tested? What should reviewers check? -->
14
15- [ ] Tested locally
16- [ ] No new warnings or errors in build
17- [ ] Existing functionality still works
18
19## Screenshots
20
21<!-- If UI changes, add before/after screenshots. Delete this section if not applicable. -->
Added.github/dependabot.yml+56−0View fileUnifiedSplit
1version: 2
2updates:
3 # Website (Next.js / npm)
4 - package-ecosystem: "npm"
5 directory: "/"
6 schedule:
7 interval: "weekly"
8 day: "monday"
9 open-pull-requests-limit: 10
10 labels:
11 - "dependencies"
12 groups:
13 react:
14 patterns:
15 - "react"
16 - "react-dom"
17 nextjs:
18 patterns:
19 - "next"
20 - "eslint-config-next"
21 tailwind:
22 patterns:
23 - "tailwindcss"
24 - "@tailwindcss/*"
25
26 # Desktop app (Tauri / Cargo)
27 - package-ecosystem: "cargo"
28 directory: "/tauri-app/src-tauri"
29 schedule:
30 interval: "weekly"
31 day: "monday"
32 open-pull-requests-limit: 5
33 labels:
34 - "dependencies"
35 - "rust"
36
37 # Shared Rust library (Cargo)
38 - package-ecosystem: "cargo"
39 directory: "/shared-rust"
40 schedule:
41 interval: "weekly"
42 day: "monday"
43 open-pull-requests-limit: 5
44 labels:
45 - "dependencies"
46 - "rust"
47
48 # GitHub Actions
49 - package-ecosystem: "github-actions"
50 directory: "/"
51 schedule:
52 interval: "weekly"
53 open-pull-requests-limit: 5
54 labels:
55 - "dependencies"
56 - "ci"
Modified.github/workflows/build-desktop.yml+4−4View fileUnifiedSplit
2929
3030 - uses: actions/setup-node@v4
3131 with:
32 node-version: 20
32 node-version: 22
3333
3434 - name: Install dependencies
3535 working-directory: desktop
4646 GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4747
4848 - name: Upload macOS artifact
49 uses: actions/upload-artifact@v4
49 uses: actions/upload-artifact@v7
5050 with:
5151 name: mac-installer
5252 path: desktop/dist/48co-mac.dmg
5959
6060 - uses: actions/setup-node@v4
6161 with:
62 node-version: 20
62 node-version: 22
6363
6464 - name: Install dependencies
6565 working-directory: desktop
7676 GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
7777
7878 - name: Upload Windows artifact
79 uses: actions/upload-artifact@v4
79 uses: actions/upload-artifact@v7
8080 with:
8181 name: win-installer
8282 path: desktop/dist/48co-win.exe
Modified.github/workflows/build-tauri.yml+1−1View fileUnifiedSplit
3434
3535 - uses: actions/setup-node@v4
3636 with:
37 node-version: 20
37 node-version: 22
3838
3939 - uses: dtolnay/rust-toolchain@stable
4040
Added.github/workflows/ci.yml+62−0View fileUnifiedSplit
1name: CI
2
3on:
4 pull_request:
5 branches: [main]
6 push:
7 branches: [main]
8
9concurrency:
10 group: ci-${{ github.ref }}
11 cancel-in-progress: true
12
13jobs:
14 lint-and-build:
15 name: Lint & Build (Website)
16 runs-on: ubuntu-latest
17 steps:
18 - uses: actions/checkout@v4
19
20 - uses: actions/setup-node@v4
21 with:
22 node-version: 22
23 cache: npm
24
25 - name: Install dependencies
26 run: npm ci
27
28 - name: Lint
29 run: npm run lint
30
31 - name: Build
32 run: npm run build
33
34 rust-check:
35 name: Rust Check (Desktop & Shared)
36 runs-on: ubuntu-latest
37 steps:
38 - uses: actions/checkout@v4
39
40 - uses: dtolnay/rust-toolchain@stable
41
42 - uses: Swatinem/rust-cache@v2
43 with:
44 workspaces: |
45 tauri-app/src-tauri
46 shared-rust
47
48 - name: Check shared-rust
49 working-directory: shared-rust
50 run: cargo check --all-targets
51
52 - name: Check tauri-app
53 working-directory: tauri-app/src-tauri
54 run: cargo check --all-targets
55
56 - name: Clippy (shared-rust)
57 working-directory: shared-rust
58 run: cargo clippy -- -D warnings
59
60 - name: Clippy (tauri-app)
61 working-directory: tauri-app/src-tauri
62 run: cargo clippy -- -D warnings
Modified.github/workflows/deploy-website.yml+1−1View fileUnifiedSplit
1919
2020 - uses: actions/setup-node@v4
2121 with:
22 node-version: 20
22 node-version: 22
2323
2424 - name: Install dependencies
2525 run: npm install
Modified.gitignore+70−4View fileUnifiedSplit
1# ── Dependencies ──────────────────────────
12node_modules/
3
4# ── Next.js ───────────────────────────────
25.next/
3desktop/dist/
4desktop/node_modules/
6out/
7.vercel/
8
9# ── Rust / Cargo ──────────────────────────
10target/
11shared-rust/target/
12tauri-app/src-tauri/target/
13
14# ── Build artifacts ───────────────────────
515*.dmg
616*.exe
717*.msi
18*.app
19*.deb
20*.AppImage
21dist/
22desktop/dist/
23desktop/node_modules/
824
9# Generated icons (created by npm run icons in desktop/)
25# ── Generated icons ──────────────────────
1026desktop/assets/icon.png
1127desktop/assets/tray-icon.png
1228desktop/assets/tray-iconTemplate.png
1329desktop/assets/tray-iconTemplate@2x.png
14shared-rust/target/
30
31# ── Environment & secrets ─────────────────
32.env
33.env.local
34.env.development
35.env.production
36.env*.local
37*.pem
38*.key
39*.p12
40
41# ── IDE & editor ─────────────────────────
42.vscode/
43.idea/
44*.swp
45*.swo
46*~
47.DS_Store
48
49# ── OS files ─────────────────────────────
50.DS_Store
51Thumbs.db
52Desktop.ini
53
54# ── Logs ──────────────────────────────────
55*.log
56npm-debug.log*
57yarn-debug.log*
58yarn-error.log*
59
60# ── Database ──────────────────────────────
61*.db
62*.sqlite
63*.sqlite3
64
65# ── Whisper models (large binary files) ───
66*.bin
67!public/**/*.bin
68
69# ── Testing ───────────────────────────────
70coverage/
71.nyc_output/
72
73# ── Mobile build artifacts ────────────────
74ios-keyboard/build/
75android-keyboard/build/
76android-keyboard/.gradle/
77android-keyboard/local.properties
78*.ipa
79*.apk
80*.aab
ModifiedCLAUDE.md+47−13View fileUnifiedSplit
3636- Website: auth, grammar API, rewrite API, live demo — working
3737- "Preserve My Voice" feature — unique, no competitor has this
3838
39### Deep Audit Results — March 29, 2026 (Component Upgrade Audit)
40**Outdated components found and upgraded this session:**
41- CRITICAL: Claude Sonnet model was `claude-sonnet-4-20250514` (10 months old). Upgraded to `claude-sonnet-4-6` across all 4 integration points.
42- CRITICAL: Anthropic API version was `2023-06-01` (3 YEARS old). Upgraded to `2025-09-01` across all 6 files.
43- HIGH: Next.js was 14.2 (2 major versions behind). Upgraded to 15.3.
44- HIGH: React was 18.3 (1 major version behind). Upgraded to 19.1.
45- HIGH: Tailwind CSS was 3.4 (1 major version behind). Upgraded to 4.1 with new CSS-first architecture.
46- HIGH: Vite was 5.4 (1 major version behind). Upgraded to 6.2.
47- MEDIUM: ESLint was 8.x. Upgraded to 9.x.
48- MEDIUM: Node.js in CI was 20. Upgraded to 22 LTS.
49- MEDIUM: Rust crates outdated — enigo 0.2→0.3, whisper-rs 0.12→0.13, dirs 5→6, candle 0.8→0.9, uniffi 0.28→0.29.
50- **Rule 13 added**: Strictest rule — no old technology allowed. Rip it out, put in the new.
51
3952### Deep Audit Results — March 28, 2026
4053**Bugs found and fixed this session:**
4154- CRITICAL: grammar.js crashed — referenced variables that were never declared (`correctionsToday`, `maxFreeCorrections`). Fixed.
104117- **NEW**: If the pricing page promises a feature that doesn't exist in code — that's a critical gap. Fix the code or fix the marketing. No lies.
105118- **NEW**: If a competitor has a feature we don't, document it in the gap list below and build a plan to beat it.
106119
107### Rule 5: Technology Currency — Bleeding Edge Only
120### Rule 5: Technology Currency — Bleeding Edge Only (STRICT — NO EXCEPTIONS)
108121We don't use "good enough" technology. We use the BEST available.
109122- Review all dependencies every session for security vulnerabilities and newer versions
110123- Check if browser APIs have changed (SpeechRecognition, Clipboard, etc.)
111124- If a library we're using has a better alternative, migrate immediately
112125- Stay on latest stable versions of ALL frameworks
113- **NEW**: Mandatory technology choices for 2026 (see Technology Mandate below)
114- **NEW**: If you find deprecated APIs in our code, replace them in the same session
126- **STRICT**: Mandatory technology choices for 2026 (see Technology Mandate below)
127- **STRICT**: If you find deprecated APIs in our code, replace them in the same session
128- **STRICT**: No raw HTML pages. Use React/JSX components for all UI. The ONLY exceptions are files that the platform forces to be HTML (Chrome extension manifest requires popup.html, offscreen.html; Vite requires index.html as SPA entry). These platform-mandated HTML files must be minimal shells that load JS components — no inline styles, no inline scripts, no UI logic in HTML.
129- **STRICT**: No old-generation frameworks, libraries, or patterns. If it was current in 2024 but superseded in 2026, rip it out. We only ship with the most advanced, fastest components available RIGHT NOW.
130- **STRICT**: Every session must audit AI model versions, framework versions, dependency versions, and API versions. If anything is not the latest — upgrade it immediately. No excuses, no delays.
115131
116132### Rule 6: Explain Like You're Not A Developer
117133All communication in plain English.
1751917. **DOCUMENT** — Record what changed and why
1761928. **UPDATE CLAUDE.md** — Record scan results, gaps found, decisions made
177193
194### Rule 13: No Old Technology — Rip It Out, Put In The New (STRICTEST RULE)
195This is the strictest rule in the entire project. No exceptions. No debate.
196- **NO raw HTML for UI**. All user interfaces must use modern component frameworks (React/JSX). The only HTML files allowed are platform-mandated shells (Chrome extension popup.html/offscreen.html, Vite index.html) — and those must be minimal loaders with ZERO UI logic.
197- **NO old frameworks**. If a framework has a newer major version, upgrade immediately. Next.js, React, Tailwind, Vite, ESLint — always latest stable.
198- **NO old AI models**. Always use the latest Claude, Whisper, Deepgram models. Check every session.
199- **NO old API versions**. Anthropic API, OpenAI API, Stripe API — always the latest version header.
200- **NO old dependencies**. Rust crates, npm packages — audit every session. If a newer version exists, upgrade.
201- **NO old Node.js**. CI/CD pipelines must run on the current LTS version.
202- **NO old patterns**. innerHTML is banned in new code — use DOM APIs or component rendering. execCommand() is banned — use modern Selection/Range APIs. var is banned — use const/let. CommonJS require() is banned in frontend — use ES modules.
203- **The standard**: If a component, library, framework, model, or API version was released more than 6 months ago AND a newer version exists — it is OLD and must be replaced.
204- **How to enforce**: Every session starts with a version audit. Every commit must use current technology. If old tech is found during any task, stop and upgrade it BEFORE continuing.
205- **Why**: We are building a professional tool for lawyers, doctors, and executives. They pay for the best. We deliver the best. Old technology is slow technology. Slow technology loses customers. We don't lose customers.
206
178207---
179208
180209## TECHNOLOGY MANDATE — What We Use and Why (2026)
181210
182211These are locked-in decisions. Do not deviate without documenting why.
183212
184### Desktop App: Tauri 2.0 (Rust + React + Vite)
213### Desktop App: Tauri 2.0 (Rust + React 19 + Vite 6)
185214- **Why**: 5MB app vs 150MB Electron. Pure Rust backend. One codebase for Windows + Mac.
186215- **Status**: Production-ready. 924 lines of solid Rust.
187216- **Kill**: Delete the legacy Electron desktop/ folder. It's dead weight.
188217- **Audio**: cpal 0.15 (Cross-Platform Audio Library)
189- **Keyboard Simulation**: enigo 0.2 (cross-platform, no nut-tree issues)
190- **Local Speech-to-Text**: whisper-rs 0.12 (whisper.cpp bindings) — supports 6 model sizes
218- **Keyboard Simulation**: enigo 0.3 (cross-platform, latest)
219- **Local Speech-to-Text**: whisper-rs 0.13 (whisper.cpp bindings, latest) — supports 6 model sizes
191220- **Local Grammar**: 91 regex rules + Claude API fallback
192221- **Hotkeys**: rdev 0.5 for global input (keyboard + mouse buttons)
222- **Frontend**: React 19.1 + Vite 6.2 + Tailwind CSS 4.1 (all latest)
193223
194224### Mobile App: Native Swift (iOS) + Native Kotlin (Android)
195225- **Why**: Custom keyboards REQUIRE native code. Expo/React Native cannot build a system-level keyboard that appears in every app.
204234- **Fix needed**: Replace deprecated execCommand() with Selection/Range API
205235- **Fix needed**: Restrict CORS to our specific extension ID, not all extensions
206236
207### Website: Next.js 14 + Vercel
208- **Status**: Working. Auth, grammar API, rewrite API, live demo all functional.
237### Website: Next.js 15.3 + React 19.1 + Tailwind CSS 4.1 + Vercel
238- **Status**: Working. Auth, grammar API, rewrite API, live demo all functional. All frameworks upgraded to latest March 2026.
209239- **Kill**: The legacy Express API server in api/ duplicates Next.js routes. Consolidate to ONE backend.
210240- **Add**: Stripe integration for payments (critical — can't make money without it)
211241- **Add**: Email verification on signup
212242- **Add**: Logout endpoint
213243- **Fix**: Google OAuth must fail-safe when GOOGLE_CLIENT_ID is missing
214244
215### AI Models — Always Latest
216- **Grammar checking**: Claude Haiku (latest version) — fast, cheap, accurate
217- **AI rewrite**: Claude Sonnet (latest version) — best writing quality
218- **Speech-to-text cloud**: OpenAI Whisper API (latest) — fallback when local model not downloaded
219- **Speech-to-text local**: whisper.cpp via whisper-rs — privacy mode, offline capable
245### AI Models — Always Latest (STRICT — Audit Every Session)
246- **Grammar checking**: Claude Haiku 4.5 (`claude-haiku-4-5-20251001`) — fast, cheap, accurate. Current latest as of March 2026.
247- **AI rewrite**: Claude Sonnet 4.6 (`claude-sonnet-4-6`) — best writing quality. Upgraded March 29, 2026.
248- **Translation**: Claude Sonnet 4.6 (`claude-sonnet-4-6`) — 200+ languages, domain-aware. Upgraded March 29, 2026.
249- **Anthropic API version**: `2025-09-01` — upgraded from ancient `2023-06-01` on March 29, 2026.
250- **Speech-to-text cloud**: OpenAI Whisper API (`whisper-1`) — fallback when local model not downloaded
251- **Speech-to-text local**: whisper.cpp via whisper-rs 0.13 — privacy mode, offline capable
252- **Real-time streaming**: Deepgram Nova-3 — word-by-word live transcription
220253- **Future local grammar**: Evaluate latest small language models (Phi-4, Gemma 3, Llama 4 Mini) — pick whichever benchmarks best for grammar correction in 2026
254- **RULE**: If a newer Claude model is released (e.g., Haiku 4.6, Opus), upgrade in the SAME SESSION. No waiting.
221255
222256### Real-Time Streaming (NEW — Required to Beat Competitors)
223257- **Cloud**: Deepgram Nova-3 or AssemblyAI Universal-2 for real-time streaming transcription
AddedSECURITY.md+34−0View fileUnifiedSplit
1# Security Policy
2
3## Reporting a Vulnerability
4
5If you discover a security vulnerability in 48co Voice, please report it responsibly.
6
7**Email:** [support@48co.nz](mailto:support@48co.nz)
8
9Include:
10- Description of the vulnerability
11- Steps to reproduce
12- Potential impact
13- Suggested fix (if any)
14
15We will acknowledge your report within 48 hours and aim to resolve critical issues within 7 days.
16
17## Supported Versions
18
19| Component | Version | Supported |
20|-----------|---------|-----------|
21| Website (Next.js) | Latest on main | Yes |
22| Desktop App (Tauri) | Latest release | Yes |
23| Chrome Extension | Latest release | Yes |
24| iOS Keyboard | Latest release | Yes |
25| Android Keyboard | Latest release | Yes |
26
27## Security Practices
28
29- Passwords are hashed with bcrypt
30- Session tokens are cryptographically random with 30-day expiry
31- All data in transit is encrypted with TLS 1.3
32- Offline mode processes everything on-device with zero cloud transmission
33- API keys are never committed to the repository
34- Dependencies are monitored via Dependabot for known vulnerabilities
Modifiedapp/api/grammar/route.js+1−1View fileUnifiedSplit
2525 headers: {
2626 'Content-Type': 'application/json',
2727 'x-api-key': claudeKey,
28 'anthropic-version': '2023-06-01',
28 'anthropic-version': '2025-09-01',
2929 },
3030 body: JSON.stringify({
3131 model: 'claude-haiku-4-5-20251001',
Modifiedapp/api/rewrite/route.js+2−2View fileUnifiedSplit
3535 headers: {
3636 'Content-Type': 'application/json',
3737 'x-api-key': claudeKey,
38 'anthropic-version': '2023-06-01',
38 'anthropic-version': '2025-09-01',
3939 },
4040 body: JSON.stringify({
41 model: 'claude-sonnet-4-20250514',
41 model: 'claude-sonnet-4-6',
4242 max_tokens: 1024,
4343 system: (prompts[mode] || prompts.professional) + voiceContext,
4444 messages: [{ role: 'user', content: text }],
Modifiedapp/api/translate/route.js+2−2View fileUnifiedSplit
6161 headers: {
6262 'Content-Type': 'application/json',
6363 'x-api-key': claudeKey,
64 'anthropic-version': '2023-06-01',
64 'anthropic-version': '2025-09-01',
6565 },
6666 body: JSON.stringify({
67 model: 'claude-sonnet-4-20250514',
67 model: 'claude-sonnet-4-6',
6868 max_tokens: 2048,
6969 system: systemPrompt,
7070 messages: [{ role: 'user', content: text }],
Modifiedapp/compare/page.jsx+46−47View fileUnifiedSplit
1import Link from 'next/link'
2
13export const metadata = {
24 title: '48co vs Grammarly vs Wispr Flow vs SuperWhisper — 2026 Comparison',
35 description: 'Honest comparison of AI grammar and voice-to-text tools in 2026. See how 48co compares on price, features, privacy, and AI quality.',
2527 { name: 'Offline Mode', co: true, gram: false, wispr: false, sw: true },
2628 { name: 'Privacy (local-first)', co: true, gram: false, wispr: false, sw: true },
2729 { name: 'Windows + Mac', co: true, gram: true, wispr: true, sw: true },
28 { name: '50+ Languages', co: true, gram: true, wispr: true, sw: true },
30 { name: '200+ Languages', co: true, gram: true, wispr: true, sw: true },
2931 { name: 'Custom Vocabulary', co: true, gram: false, wispr: true, sw: true },
30 { name: 'Developer Mode', co: true, gram: false, wispr: false, sw: false, highlight: true },
31 { name: 'AI Engine', co: 'Claude', gram: 'Proprietary', wispr: 'Mixed', sw: 'Whisper' },
3232 { name: 'Real-time Translation', co: true, gram: false, wispr: false, sw: false, highlight: true },
33 { name: 'Real-time Streaming', co: true, gram: false, wispr: true, sw: true },
34 { name: 'Meeting Transcription', co: 'Coming soon', gram: false, wispr: false, sw: false },
33 { name: 'AI Engine', co: 'Claude', gram: 'Proprietary', wispr: 'Mixed', sw: 'Whisper' },
3534 ]
3635
3736 function renderCell(val) {
38 if (val === true) return <span className="text-green-600 font-medium">Yes</span>
37 if (val === true) return <span className="text-emerald-600 font-medium">Yes</span>
3938 if (val === false) return <span className="text-gray-300">No</span>
40 return <span className="text-gray-500">{val}</span>
39 return <span className="text-gray-600">{val}</span>
4140 }
4241
4342 return (
4443 <main className="min-h-screen bg-white">
4544 <Nav />
4645
47 <div className="max-w-5xl mx-auto px-4 pt-28 pb-16">
48 <div className="text-center mb-12">
49 <h1 className="text-4xl font-bold text-gray-900 mb-3">
50 How <span className="text-indigo-600">48co</span> compares
46 <div className="max-w-5xl mx-auto px-4 pt-32 pb-16">
47 <div className="text-center mb-14">
48 <h1 className="text-4xl md:text-5xl font-bold text-navy-900 mb-4">
49 How <span className="text-gold-500">48co</span> compares
5150 </h1>
52 <p className="text-gray-400 text-base max-w-lg mx-auto">
51 <p className="text-gray-500 text-base max-w-lg mx-auto">
5352 An honest comparison. We highlight where competitors beat us too.
5453 </p>
5554 </div>
5655
5756 {/* Comparison Table */}
58 <div className="border border-gray-200 rounded-2xl overflow-hidden overflow-x-auto">
57 <div className="border border-gray-200 rounded-xl overflow-hidden overflow-x-auto">
5958 <table className="w-full text-[13px]">
6059 <thead>
61 <tr className="border-b border-gray-100 bg-gray-50">
62 <th className="text-left py-3 px-4 text-gray-400 font-normal w-[180px]">Feature</th>
63 <th className="text-center py-3 px-4 text-indigo-600 font-bold">48co</th>
64 <th className="text-center py-3 px-4 text-gray-400 font-normal">Grammarly</th>
65 <th className="text-center py-3 px-4 text-gray-400 font-normal">Wispr Flow</th>
66 <th className="text-center py-3 px-4 text-gray-400 font-normal">SuperWhisper</th>
60 <tr className="border-b border-gray-100 bg-[#FAFAF8]">
61 <th className="text-left py-3.5 px-4 text-gray-500 font-medium w-[180px]">Feature</th>
62 <th className="text-center py-3.5 px-4 text-navy-900 font-bold">48co</th>
63 <th className="text-center py-3.5 px-4 text-gray-400 font-medium">Grammarly</th>
64 <th className="text-center py-3.5 px-4 text-gray-400 font-medium">Wispr Flow</th>
65 <th className="text-center py-3.5 px-4 text-gray-400 font-medium">SuperWhisper</th>
6766 </tr>
6867 </thead>
6968 <tbody>
7069 {features.map((f) => (
71 <tr key={f.name} className={`border-b border-gray-50 ${f.highlight ? 'bg-indigo-50/40' : ''}`}>
72 <td className="py-2.5 px-4 text-gray-500">{f.name}</td>
73 <td className="py-2.5 px-4 text-center font-medium">{renderCell(f.co)}</td>
74 <td className="py-2.5 px-4 text-center">{renderCell(f.gram)}</td>
75 <td className="py-2.5 px-4 text-center">{renderCell(f.wispr)}</td>
76 <td className="py-2.5 px-4 text-center">{renderCell(f.sw)}</td>
70 <tr key={f.name} className={`border-b border-gray-50 hover:bg-gray-50/50 transition-colors ${f.highlight ? 'bg-gold-50/20' : ''}`}>
71 <td className="py-3 px-4 text-gray-600">{f.name}</td>
72 <td className="py-3 px-4 text-center font-medium">{renderCell(f.co)}</td>
73 <td className="py-3 px-4 text-center">{renderCell(f.gram)}</td>
74 <td className="py-3 px-4 text-center">{renderCell(f.wispr)}</td>
75 <td className="py-3 px-4 text-center">{renderCell(f.sw)}</td>
7776 </tr>
7877 ))}
7978 </tbody>
8281
8382 {/* Honest Takes */}
8483 <div className="mt-16 grid md:grid-cols-2 gap-6">
85 <div className="rounded-2xl p-6 border border-gray-200">
86 <h2 className="text-[15px] font-bold text-gray-800 mb-3">Where Grammarly beats us (for now)</h2>
87 <ul className="space-y-2 text-[13px] text-gray-400">
84 <div className="rounded-xl p-6 border border-gray-200">
85 <h2 className="text-[15px] font-bold text-navy-900 mb-3">Where Grammarly beats us (for now)</h2>
86 <ul className="space-y-2 text-[13px] text-gray-500">
8887 <li>+ Microsoft Office plugin (Word, Outlook)</li>
8988 <li>+ 15+ years of user data training their models</li>
9089 <li>+ Brand recognition — everyone knows Grammarly</li>
9190 </ul>
92 <p className="mt-3 text-[11px] text-gray-300">But: $30/mo, no voice-to-text, no desktop typing, rules-based not AI-native.</p>
91 <p className="mt-4 text-[12px] text-gray-400">But: $30/mo, no voice-to-text, no desktop typing, rules-based not AI-native.</p>
9392 </div>
9493
95 <div className="rounded-2xl p-6 border border-gray-200">
96 <h2 className="text-[15px] font-bold text-gray-800 mb-3">Where Wispr Flow beats us (for now)</h2>
97 <ul className="space-y-2 text-[13px] text-gray-400">
94 <div className="rounded-xl p-6 border border-gray-200">
95 <h2 className="text-[15px] font-bold text-navy-900 mb-3">Where Wispr Flow beats us (for now)</h2>
96 <ul className="space-y-2 text-[13px] text-gray-500">
9897 <li>+ Established iOS/Android apps with large user base</li>
9998 <li>+ 200+ app integrations</li>
10099 <li>+ $81M in VC funding = fast development</li>
101100 </ul>
102 <p className="mt-3 text-[11px] text-gray-300">But: No grammar checking, cloud-only (no privacy), $15/mo.</p>
101 <p className="mt-4 text-[12px] text-gray-400">But: No grammar checking, cloud-only (no privacy), $15/mo.</p>
103102 </div>
104103
105 <div className="rounded-2xl p-6 border-2 border-indigo-200 bg-indigo-50/30 md:col-span-2">
106 <h2 className="text-[15px] font-bold text-indigo-700 mb-4">Where 48co wins</h2>
107 <div className="grid md:grid-cols-4 gap-4">
104 <div className="rounded-xl p-7 border-2 border-navy-200 bg-navy-50/30 md:col-span-2">
105 <h2 className="text-[15px] font-bold text-navy-900 mb-5">Where 48co wins</h2>
106 <div className="grid md:grid-cols-4 gap-5">
108107 <div>
109 <h3 className="text-[13px] text-gray-700 font-semibold mb-1">Grammar + Voice</h3>
110 <p className="text-[12px] text-gray-400">Only tool that does AI grammar AND voice-to-text. Grammarly can&apos;t dictate. Wispr can&apos;t grammar check.</p>
108 <h3 className="text-[13px] text-navy-800 font-semibold mb-1">Grammar + Voice</h3>
109 <p className="text-[12px] text-gray-500">Only tool that does AI grammar AND voice-to-text. Grammarly can&apos;t dictate. Wispr can&apos;t grammar check.</p>
111110 </div>
112111 <div>
113 <h3 className="text-[13px] text-gray-700 font-semibold mb-1">Context-Aware</h3>
114 <p className="text-[12px] text-gray-400">Auto-detects which app you&apos;re in. Professional for email, casual for Slack, technical for code.</p>
112 <h3 className="text-[13px] text-navy-800 font-semibold mb-1">Context-Aware</h3>
113 <p className="text-[12px] text-gray-500">Auto-detects which app you&apos;re in. Professional for email, casual for Slack, technical for code.</p>
115114 </div>
116115 <div>
117 <h3 className="text-[13px] text-gray-700 font-semibold mb-1">$29/mo for 10 users</h3>
118 <p className="text-[12px] text-gray-400">Business plan: $2.90/user. Grammarly charges $15/user ($150/mo for 10). We&apos;re 80% cheaper.</p>
116 <h3 className="text-[13px] text-navy-800 font-semibold mb-1">$29/mo for 10 users</h3>
117 <p className="text-[12px] text-gray-500">Business plan: $2.90/user. Grammarly charges $15/user ($150/mo for 10). We&apos;re 80% cheaper.</p>
119118 </div>
120119 <div>
121 <h3 className="text-[13px] text-gray-700 font-semibold mb-1">Claude AI</h3>
122 <p className="text-[12px] text-gray-400">Powered by the latest Claude model. Smarter corrections, better tone detection, more natural rewrites.</p>
120 <h3 className="text-[13px] text-navy-800 font-semibold mb-1">200+ Languages</h3>
121 <p className="text-[12px] text-gray-500">Real-time translation with domain-aware terminology for legal, medical, and finance contexts.</p>
123122 </div>
124123 </div>
125124 </div>
127126
128127 {/* CTA */}
129128 <div className="mt-16 text-center">
130 <a href="/download" className="inline-block px-8 py-3 rounded-xl bg-indigo-600 text-white text-[15px] font-medium hover:bg-indigo-500 transition-all shadow-sm">
129 <Link href="/download" className="inline-block px-8 py-3.5 rounded-lg bg-navy-900 text-white text-[15px] font-semibold hover:bg-navy-800 transition-all">
131130 Download 48co Free
132 </a>
133 <p className="text-[12px] text-gray-300 mt-3">Mac + Windows + Chrome. Free tier available. No credit card.</p>
131 </Link>
132 <p className="text-[12px] text-gray-400 mt-3">Mac + Windows + Chrome + iOS + Android. Free tier. No credit card.</p>
134133 </div>
135134 </div>
136135
Modifiedapp/download/page.jsx+48−48View fileUnifiedSplit
11'use client'
22
33import { useState, useEffect } from 'react'
4import Link from 'next/link'
45import Nav from '../../components/Nav'
56import Footer from '../../components/Footer'
67
2122 'Launch 48co — it appears in your menu bar (top-right)',
2223 'Allow Accessibility + Microphone when prompted',
2324 'Right-click the menu bar icon and sign in',
24 'Press Cmd+Shift+Space anywhere to start talking',
25 'Press Cmd+Shift+Space anywhere to start dictating',
2526 ],
2627 windows: [
2728 'Click the Download button to get the installer',
2829 'Run it — click "Yes" if Windows asks permission',
2930 '48co appears in your system tray (bottom-right near the clock)',
3031 'Right-click the tray icon and sign in',
31 'Press Ctrl+Shift+Space anywhere to start talking',
32 'Press Ctrl+Shift+Space anywhere to start dictating',
3233 ],
3334 }
3435
3637 <main className="min-h-screen bg-white">
3738 <Nav />
3839
39 <div className="max-w-3xl mx-auto px-4 pt-28 pb-16">
40 <div className="max-w-3xl mx-auto px-4 pt-32 pb-16">
4041 {/* Hero */}
41 <div className="text-center mb-12">
42 <h1 className="text-4xl font-bold text-gray-900 mb-3">
43 Download <span className="text-indigo-600">48co</span>
42 <div className="text-center mb-14">
43 <h1 className="text-4xl md:text-5xl font-bold text-navy-900 mb-4">
44 Download <span className="text-gold-500">48co</span>
4445 </h1>
45 <p className="text-gray-400 text-base max-w-md mx-auto">
46 AI grammar + voice-to-text that works in every app on your computer. Free to start.
46 <p className="text-gray-500 text-base max-w-md mx-auto">
47 AI grammar, voice dictation, and translation that works in every app on your computer. Free to start.
4748 </p>
4849 </div>
4950
5051 {/* Download Buttons */}
51 <div className="flex flex-col md:flex-row gap-4 justify-center mb-12">
52 <div className="flex flex-col md:flex-row gap-4 justify-center mb-14">
5253 <a
5354 href="https://github.com/ccantynz-alt/-48co-ai-pa/releases/latest/download/48co-mac.dmg"
54 className={`flex items-center gap-4 px-8 py-4 rounded-2xl border transition-all ${
55 className={`flex items-center gap-4 px-8 py-5 rounded-xl border transition-all ${
5556 platform === 'mac'
56 ? 'bg-indigo-50 border-indigo-200 shadow-md shadow-indigo-500/5'
57 ? 'bg-navy-50 border-navy-200 shadow-md shadow-navy-500/5'
5758 : 'border-gray-200 hover:border-gray-300'
5859 }`}
5960 >
60 <svg width="28" height="28" viewBox="0 0 24 24" fill="#333" opacity="0.6">
61 <svg width="28" height="28" viewBox="0 0 24 24" fill="#0B1A2E" opacity="0.5">
6162 <path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/>
6263 </svg>
6364 <div>
64 <p className="text-[15px] font-semibold text-gray-800">Download for macOS</p>
65 <p className="text-[12px] text-gray-400">Intel + Apple Silicon (.dmg)</p>
65 <p className="text-[15px] font-semibold text-navy-900">Download for macOS</p>
66 <p className="text-[12px] text-gray-500">Intel + Apple Silicon (.dmg)</p>
6667 </div>
67 {platform === 'mac' && <span className="text-[10px] text-indigo-500 ml-auto font-medium">Recommended</span>}
68 {platform === 'mac' && <span className="text-[10px] text-gold-600 ml-auto font-semibold">Recommended</span>}
6869 </a>
6970
7071 <a
7172 href="https://github.com/ccantynz-alt/-48co-ai-pa/releases/latest/download/48co-win.exe"
72 className={`flex items-center gap-4 px-8 py-4 rounded-2xl border transition-all ${
73 className={`flex items-center gap-4 px-8 py-5 rounded-xl border transition-all ${
7374 platform === 'windows'
74 ? 'bg-indigo-50 border-indigo-200 shadow-md shadow-indigo-500/5'
75 ? 'bg-navy-50 border-navy-200 shadow-md shadow-navy-500/5'
7576 : 'border-gray-200 hover:border-gray-300'
7677 }`}
7778 >
78 <svg width="28" height="28" viewBox="0 0 24 24" fill="#333" opacity="0.6">
79 <svg width="28" height="28" viewBox="0 0 24 24" fill="#0B1A2E" opacity="0.5">
7980 <path d="M3 12V6.75l8-1.25V12H3zm0 .5h8v6.5l-8-1.25V12.5zM11.5 5.33L21 3.75V12h-9.5V5.33zm0 7.17H21v8.25l-9.5-1.58V12.5z"/>
8081 </svg>
8182 <div>
82 <p className="text-[15px] font-semibold text-gray-800">Download for Windows</p>
83 <p className="text-[12px] text-gray-400">Windows 10+ (.exe)</p>
83 <p className="text-[15px] font-semibold text-navy-900">Download for Windows</p>
84 <p className="text-[12px] text-gray-500">Windows 10+ (.exe)</p>
8485 </div>
85 {platform === 'windows' && <span className="text-[10px] text-indigo-500 ml-auto font-medium">Recommended</span>}
86 {platform === 'windows' && <span className="text-[10px] text-gold-600 ml-auto font-semibold">Recommended</span>}
8687 </a>
8788 </div>
8889
8990 {/* Also available */}
90 <div className="flex flex-wrap justify-center gap-3 mb-16">
91 <a href="/install" className="text-[12px] px-4 py-2 rounded-lg border border-gray-200 text-gray-400 hover:border-gray-300 hover:text-gray-600 transition-all">
91 <div className="flex flex-wrap justify-center gap-3 mb-20">
92 <Link href="/install" className="text-[12px] px-4 py-2 rounded-lg border border-gray-200 text-gray-500 hover:border-navy-200 hover:text-navy-700 transition-all font-medium">
9293 Chrome Extension
93 </a>
94 <a href="/live" className="text-[12px] px-4 py-2 rounded-lg border border-gray-200 text-gray-400 hover:border-gray-300 hover:text-gray-600 transition-all">
94 </Link>
95 <Link href="/live" className="text-[12px] px-4 py-2 rounded-lg border border-gray-200 text-gray-500 hover:border-navy-200 hover:text-navy-700 transition-all font-medium">
9596 Try in Browser (no download)
96 </a>
97 </Link>
9798 </div>
9899
99100 {/* Setup Steps */}
100 <div className="mb-16">
101 <h2 className="text-xl font-bold text-gray-900 mb-6 text-center">Setup in {tab === 'mac' ? '6' : '5'} steps</h2>
101 <div className="mb-20">
102 <h2 className="text-xl font-bold text-navy-900 mb-8 text-center">Setup in {tab === 'mac' ? '6' : '5'} steps</h2>
102103
103 <div className="flex justify-center gap-2 mb-6">
104 <div className="flex justify-center gap-2 mb-8">
104105 <button
105106 onClick={() => setTab('mac')}
106 className={`px-4 py-2 rounded-lg text-[13px] font-medium transition-all ${
107 tab === 'mac' ? 'bg-indigo-50 text-indigo-600 border border-indigo-200' : 'text-gray-400 border border-gray-200 hover:border-gray-300'
107 className={`px-5 py-2 rounded-lg text-[13px] font-medium transition-all ${
108 tab === 'mac' ? 'bg-navy-900 text-white' : 'text-gray-500 border border-gray-200 hover:border-gray-300'
108109 }`}
109110 >
110111 macOS
111112 </button>
112113 <button
113114 onClick={() => setTab('windows')}
114 className={`px-4 py-2 rounded-lg text-[13px] font-medium transition-all ${
115 tab === 'windows' ? 'bg-indigo-50 text-indigo-600 border border-indigo-200' : 'text-gray-400 border border-gray-200 hover:border-gray-300'
115 className={`px-5 py-2 rounded-lg text-[13px] font-medium transition-all ${
116 tab === 'windows' ? 'bg-navy-900 text-white' : 'text-gray-500 border border-gray-200 hover:border-gray-300'
116117 }`}
117118 >
118119 Windows
121122
122123 <div className="max-w-lg mx-auto space-y-3">
123124 {(STEPS[tab] || STEPS.mac).map((step, i) => (
124 <div key={i} className="flex items-start gap-4 p-4 rounded-xl border border-gray-100 bg-gray-50/50">
125 <span className="w-7 h-7 rounded-lg bg-indigo-50 border border-indigo-100 flex items-center justify-center text-indigo-600 text-[12px] font-bold flex-shrink-0">
125 <div key={i} className="flex items-start gap-4 p-4 rounded-xl border border-gray-100 bg-[#FAFAF8]">
126 <span className="w-7 h-7 rounded-lg bg-navy-900 flex items-center justify-center text-white text-[12px] font-bold flex-shrink-0">
126127 {i + 1}
127128 </span>
128129 <p className="text-[13px] text-gray-600 leading-relaxed pt-0.5">{step}</p>
132133 </div>
133134
134135 {/* Features Grid */}
135 <div className="mb-16">
136 <h2 className="text-xl font-bold text-gray-900 mb-6 text-center">What you get</h2>
136 <div className="mb-20">
137 <h2 className="text-xl font-bold text-navy-900 mb-8 text-center">What you get</h2>
137138 <div className="grid md:grid-cols-2 gap-4">
138139 {[
139 { title: 'AI Grammar + Rewrite', desc: 'Fixes grammar, removes filler words, adjusts tone — all powered by Claude AI.' },
140 { title: 'AI Grammar & Rewrite', desc: 'Fixes grammar, removes filler words, adjusts tone — all powered by Claude AI.' },
140141 { title: 'Works in Every App', desc: 'Types into any focused text field — browsers, Slack, VS Code, email, Word, anything.' },
141 { title: 'Voice-to-Text', desc: 'Press a hotkey, speak naturally, text appears. 99%+ accuracy with Whisper in 50+ languages.' },
142 { title: 'Context-Aware', desc: 'Automatically detects Gmail → professional, Slack → casual, Code → technical.' },
143 { title: 'Auto-Updates', desc: 'Always up to date. New features delivered automatically, no reinstalling.' },
142 { title: 'Voice-to-Text Dictation', desc: 'Press a hotkey, speak naturally, text appears. 99%+ accuracy with Whisper in 200+ languages.' },
143 { title: 'Context-Aware', desc: 'Automatically detects Gmail for professional tone, Slack for casual, code editors for technical.' },
144 { title: 'Real-Time Translation', desc: 'Speak English, text appears in 200+ languages. Domain-aware for legal, medical, and finance.' },
144145 { title: 'Privacy First', desc: 'Local Whisper model for fully offline voice-to-text. Your voice and text never leave your device.' },
145 { title: 'Real-time Translation', desc: 'Speak English, text appears in 60+ languages. Domain-aware for legal, medical, and finance.' },
146146 ].map((f) => (
147147 <div key={f.title} className="card p-5">
148 <h3 className="text-[14px] font-semibold text-gray-800 mb-1">{f.title}</h3>
149 <p className="text-[12px] text-gray-400 leading-relaxed">{f.desc}</p>
148 <h3 className="text-[14px] font-semibold text-navy-900 mb-1">{f.title}</h3>
149 <p className="text-[12px] text-gray-500 leading-relaxed">{f.desc}</p>
150150 </div>
151151 ))}
152152 </div>
154154
155155 {/* Pricing hint */}
156156 <div className="text-center">
157 <div className="inline-block p-5 rounded-2xl bg-gray-50 border border-gray-100">
158 <p className="text-[14px] text-gray-600 font-medium mb-1">Free to start. Pro is $12/mo.</p>
159 <p className="text-[12px] text-gray-400">10 free grammar corrections per day. Upgrade for unlimited.</p>
160 <a href="/pricing" className="text-[12px] text-indigo-600 hover:text-indigo-500 font-medium mt-2 inline-block">See pricing →</a>
157 <div className="inline-block p-6 rounded-xl bg-[#FAFAF8] border border-gray-100">
158 <p className="text-[14px] text-navy-900 font-semibold mb-1">Free to start. Pro is $12/mo.</p>
159 <p className="text-[12px] text-gray-500">10 free grammar corrections per day. Upgrade for unlimited AI.</p>
160 <Link href="/pricing" className="text-[12px] text-navy-700 hover:text-navy-900 font-semibold mt-3 inline-block transition-colors">See pricing &rarr;</Link>
161161 </div>
162162 </div>
163163 </div>
Modifiedapp/globals.css+3−4View fileUnifiedSplit
1@tailwind base;
2@tailwind components;
3@tailwind utilities;
1@import "tailwindcss";
2@config "../tailwind.config.js";
43
54:root {
65 --bg: #ffffff;
6463}
6564
6665@keyframes typing-cursor {
67 0%, 50% { border-right-color: var(--accent); }
66 0%, 50% { border-right-color: var(--accent-gold); }
6867 51%, 100% { border-right-color: transparent; }
6968}
7069
Modifiedapp/install/page.jsx+23−22View fileUnifiedSplit
1import Link from 'next/link'
12import Nav from '../../components/Nav'
23import Footer from '../../components/Footer'
34
67 <main className="min-h-screen bg-white">
78 <Nav />
89
9 <div className="max-w-3xl mx-auto px-4 pt-28 pb-16">
10 <div className="text-center mb-12">
11 <h1 className="text-4xl font-bold text-gray-900 mb-3">
10 <div className="max-w-3xl mx-auto px-4 pt-32 pb-16">
11 <div className="text-center mb-14">
12 <h1 className="text-4xl md:text-5xl font-bold text-navy-900 mb-4">
1213 Chrome Extension
1314 </h1>
14 <p className="text-gray-400 text-base max-w-md mx-auto">
15 <p className="text-gray-500 text-base max-w-md mx-auto">
1516 AI grammar checking on any website. Corrects your writing in real-time as you type in Gmail, Slack, Google Docs, and everywhere else.
1617 </p>
1718 </div>
1819
1920 {/* Quick install */}
20 <div className="max-w-lg mx-auto mb-16">
21 <div className="card p-6 shadow-md shadow-black/[0.03]">
22 <h2 className="text-lg font-bold text-gray-900 mb-4">Install in 3 minutes</h2>
21 <div className="max-w-lg mx-auto mb-20">
22 <div className="card p-7 shadow-md shadow-black/[0.02]">
23 <h2 className="text-lg font-bold text-navy-900 mb-5">Install in 3 minutes</h2>
2324
24 <div className="space-y-4">
25 <div className="space-y-5">
2526 {[
2627 { step: '1', title: 'Download the extension', desc: 'Click below to download the extension package (.zip file).', action: true },
2728 { step: '2', title: 'Open Chrome Extensions', desc: 'Go to chrome://extensions in your browser. Turn on "Developer mode" (top-right toggle).' },
2930 { step: '4', title: 'Start using it', desc: 'Open any website. Start typing in a text field. 48co will check your grammar automatically.' },
3031 ].map((s) => (
3132 <div key={s.step} className="flex items-start gap-4">
32 <span className="w-8 h-8 rounded-lg bg-indigo-50 border border-indigo-100 flex items-center justify-center text-indigo-600 text-[13px] font-bold flex-shrink-0">
33 <span className="w-8 h-8 rounded-lg bg-navy-900 flex items-center justify-center text-white text-[13px] font-bold flex-shrink-0">
3334 {s.step}
3435 </span>
3536 <div className="flex-1">
36 <h3 className="text-[14px] font-semibold text-gray-800 mb-0.5">{s.title}</h3>
37 <p className="text-[12px] text-gray-400 leading-relaxed">{s.desc}</p>
37 <h3 className="text-[14px] font-semibold text-navy-900 mb-0.5">{s.title}</h3>
38 <p className="text-[12px] text-gray-500 leading-relaxed">{s.desc}</p>
3839 {s.action && (
3940 <a
4041 href="/48co-extension.zip"
4142 download
42 className="inline-block mt-2 px-4 py-1.5 rounded-lg bg-indigo-600 text-white text-[12px] font-medium hover:bg-indigo-500 transition-all"
43 className="inline-block mt-3 px-5 py-2 rounded-lg bg-navy-900 text-white text-[12px] font-semibold hover:bg-navy-800 transition-all"
4344 >
4445 Download Extension (.zip)
4546 </a>
5253 </div>
5354
5455 {/* What it does */}
55 <div className="mb-16">
56 <h2 className="text-xl font-bold text-gray-900 mb-6 text-center">What the extension does</h2>
56 <div className="mb-20">
57 <h2 className="text-xl font-bold text-navy-900 mb-8 text-center">What the extension does</h2>
5758 <div className="grid md:grid-cols-3 gap-4">
5859 {[
5960 { title: 'AI Grammar Check', desc: 'Scans every text field you type in. Shows corrections in a clean tooltip. Click to fix.' },
6061 { title: 'Voice-to-Text', desc: 'Press middle-click or Ctrl+Shift+Space to dictate. Text streams into the focused field as you speak.' },
61 { title: 'Real-time Streaming', desc: 'Deepgram Nova-3 engine option for sub-500ms latency. Words appear as you speak.' },
62 { title: 'Live Translation', desc: 'Speak English, text appears in 60+ languages. Domain-aware for legal, medical, and finance terminology.' },
62 { title: 'Live Translation', desc: 'Speak English, text appears in 200+ languages. Domain-aware for legal, medical, and finance.' },
63 { title: 'Real-Time Streaming', desc: 'Deepgram Nova-3 engine option for sub-500ms latency. Words appear as you speak.' },
6364 { title: 'Custom Vocabulary', desc: 'Add specialist terms the AI should never get wrong — legal Latin, medical terms, client names.' },
64 { title: 'Works Everywhere', desc: 'Gmail, Claude, ChatGPT, Slack, Google Docs, Twitter, LinkedIn — any website with a text field.' },
65 { title: 'Works Everywhere', desc: 'Gmail, Claude, ChatGPT, Slack, Google Docs, LinkedIn — any website with a text field.' },
6566 ].map((f) => (
6667 <div key={f.title} className="card p-5">
67 <h3 className="text-[14px] font-semibold text-gray-800 mb-1">{f.title}</h3>
68 <p className="text-[12px] text-gray-400 leading-relaxed">{f.desc}</p>
68 <h3 className="text-[14px] font-semibold text-navy-900 mb-1">{f.title}</h3>
69 <p className="text-[12px] text-gray-500 leading-relaxed">{f.desc}</p>
6970 </div>
7071 ))}
7172 </div>
7374
7475 {/* Want more? */}
7576 <div className="text-center">
76 <p className="text-[13px] text-gray-400 mb-3">Want voice-to-text in ANY app (not just the browser)?</p>
77 <a href="/download" className="inline-block px-6 py-2.5 rounded-xl bg-indigo-600 text-white text-[13px] font-medium hover:bg-indigo-500 transition-all">
77 <p className="text-[13px] text-gray-500 mb-4">Want voice-to-text in ANY app (not just the browser)?</p>
78 <Link href="/download" className="inline-block px-6 py-3 rounded-lg bg-navy-900 text-white text-[13px] font-semibold hover:bg-navy-800 transition-all">
7879 Download the Desktop App
79 </a>
80 </Link>
8081 </div>
8182 </div>
8283
Modifiedapp/layout.jsx+17−15View fileUnifiedSplit
11import './globals.css'
2import { Inter } from 'next/font/google'
3
4const inter = Inter({
5 subsets: ['latin'],
6 weight: ['400', '500', '600', '700', '800'],
7 display: 'swap',
8})
29
310export const metadata = {
4 title: '48co — AI Grammar & Voice-to-Text | Perfect Everything You Write',
5 description: 'AI grammar correction + voice-to-text that works on every device. Fixes grammar, spelling, and tone in real-time. Desktop, Chrome, iPhone, Android. Free to start.',
6 keywords: 'grammar checker, voice to text, dictation software, AI writing assistant, grammarly alternative, wispr flow alternative',
11 title: '48co Voice — AI Grammar & Dictation for Legal, Accounting & Medical Professionals',
12 description: 'Professional-grade AI grammar correction, voice-to-text dictation, and real-time translation. Built for lawyers, accountants, and medical professionals. Desktop, Chrome, iPhone, Android.',
13 keywords: 'grammar checker, voice to text, dictation software, AI writing assistant, legal dictation, medical dictation, grammarly alternative',
714 openGraph: {
8 title: '48co — AI Grammar That Works Everywhere',
9 description: 'Everything you write, perfected by AI. Grammar + voice-to-text on every device.',
15 title: '48co Voice — Professional AI Grammar & Dictation',
16 description: 'AI grammar correction + voice-to-text for professionals who write for a living. Every device, every app.',
1017 url: 'https://48co.nz',
11 siteName: '48co',
18 siteName: '48co Voice',
1219 type: 'website',
1320 },
1421 twitter: {
1522 card: 'summary_large_image',
16 title: '48co — AI Grammar & Voice-to-Text',
17 description: 'Everything you write, perfected by AI.',
23 title: '48co Voice — AI Grammar & Dictation',
24 description: 'Professional-grade AI grammar + voice-to-text. Built for lawyers, accountants, and doctors.',
1825 },
1926}
2027
2128export default function RootLayout({ children }) {
2229 return (
23 <html lang="en">
24 <head>
25 <link rel="preconnect" href="https://fonts.googleapis.com" />
26 <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
27 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
28 </head>
29 <body className="bg-white text-gray-900 antialiased" style={{ fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" }}>
30 <html lang="en" className={inter.className}>
31 <body className="bg-white text-gray-900 antialiased">
3032 {children}
3133 </body>
3234 </html>
Modifiedapp/live/page.jsx+37−36View fileUnifiedSplit
11'use client'
22
33import { useState, useEffect, useRef } from 'react'
4import Link from 'next/link'
45import Nav from '../../components/Nav'
56import Footer from '../../components/Footer'
67import Waveform from '../../components/Waveform'
8283 <main className="min-h-screen bg-white">
8384 <Nav />
8485
85 <div className="max-w-3xl mx-auto px-4 pt-28 pb-16">
86 <div className="max-w-3xl mx-auto px-4 pt-32 pb-16">
8687 {/* Hero */}
87 <div className="text-center mb-12">
88 <h1 className="text-4xl font-bold text-gray-900 mb-3">
89 Try <span className="text-indigo-600">48co</span> live
88 <div className="text-center mb-14">
89 <h1 className="text-4xl md:text-5xl font-bold text-navy-900 mb-4">
90 Try <span className="text-gold-500">48co</span> live
9091 </h1>
91 <p className="text-gray-400 text-base max-w-md mx-auto">
92 <p className="text-gray-500 text-base max-w-md mx-auto">
9293 Click the mic, speak, see your words. Runs entirely in your browser — no download or sign-up needed.
9394 </p>
9495 </div>
9596
9697 {/* Voice Recorder */}
97 <div className="max-w-lg mx-auto mb-12">
98 <div className="card overflow-hidden shadow-lg shadow-black/[0.04]">
98 <div className="max-w-lg mx-auto mb-14">
99 <div className="card overflow-hidden shadow-lg shadow-black/[0.03]">
99100 {/* Header */}
100 <div className="flex items-center justify-between px-5 py-3 border-b border-black/[0.04] bg-gray-50/50">
101 <div className="flex items-center justify-between px-5 py-3.5 border-b border-black/[0.04]">
101102 <div className="flex items-center gap-3">
102 <span className="text-[12px] font-medium text-gray-500">Live Demo</span>
103 <span className="text-[12px] font-semibold text-navy-900">Live Demo</span>
103104 <select
104105 value={language}
105106 onChange={(e) => setLanguage(e.target.value)}
106 className="text-[11px] text-gray-400 bg-transparent border border-gray-200 rounded px-2 py-0.5 outline-none"
107 className="text-[11px] text-gray-500 bg-transparent border border-gray-200 rounded-lg px-2.5 py-1 outline-none"
107108 >
108109 {LANGUAGES.map(l => <option key={l.code} value={l.code}>{l.name}</option>)}
109110 </select>
110111 </div>
111 <span className={`text-[10px] px-2 py-0.5 rounded-full border ${
112 <span className={`text-[10px] px-2.5 py-0.5 rounded-full border font-medium ${
112113 status === 'recording' ? 'border-red-200 text-red-500 bg-red-50' :
113 status === 'done' ? 'border-green-200 text-green-600 bg-green-50' :
114 status === 'done' ? 'border-emerald-200 text-emerald-600 bg-emerald-50' :
114115 'border-gray-200 text-gray-400'
115116 }`}>
116117 {status === 'recording' ? 'Listening...' : status === 'done' ? 'Done' : 'Ready'}
118119 </div>
119120
120121 {/* Waveform */}
121 <div className="py-4 border-b border-black/[0.04]">
122 <div className="py-4 border-b border-black/[0.04] bg-navy-950">
122123 <Waveform isRecording={status === 'recording'} />
123124 </div>
124125
125126 {/* Raw transcript */}
126127 {transcript && (
127128 <div className="px-5 py-3 border-b border-black/[0.04]">
128 <p className="text-[10px] text-gray-300 uppercase tracking-wider mb-1">Raw</p>
129 <p className="text-[12px] text-gray-400 leading-relaxed">{transcript}</p>
129 <p className="text-[10px] text-gray-400 uppercase tracking-wider mb-1">Raw</p>
130 <p className="text-[12px] text-gray-500 leading-relaxed">{transcript}</p>
130131 </div>
131132 )}
132133
133134 {/* Processed */}
134135 {processedText && status !== 'recording' && (
135 <div className="px-5 py-3 border-b border-black/[0.04] bg-indigo-50/30">
136 <div className="px-5 py-3 border-b border-black/[0.04] bg-navy-50/30">
136137 <div className="flex items-center justify-between mb-1">
137 <p className="text-[10px] text-indigo-400 uppercase tracking-wider">Processed</p>
138 <p className="text-[10px] text-navy-600 uppercase tracking-wider font-medium">Processed</p>
138139 <button
139140 onClick={() => navigator.clipboard?.writeText(processedText)}
140 className="text-[10px] text-indigo-400 hover:text-indigo-600 transition-colors"
141 className="text-[10px] text-navy-500 hover:text-navy-700 transition-colors font-medium"
141142 >
142143 Copy
143144 </button>
144145 </div>
145 <p className="text-[13px] text-gray-800 leading-relaxed whitespace-pre-wrap">{processedText}</p>
146 <p className="text-[13px] text-navy-900 leading-relaxed whitespace-pre-wrap">{processedText}</p>
146147 </div>
147148 )}
148149
151152 <button
152153 onClick={handleMicClick}
153154 className={`w-14 h-14 rounded-full flex items-center justify-center transition-all cursor-pointer border-2 ${
154 status === 'recording' ? 'border-red-400 bg-red-50 shadow-[0_0_20px_rgba(220,38,38,0.15)]' :
155 status === 'done' ? 'border-green-400 bg-green-50' :
156 'border-gray-200 bg-gray-50 hover:border-gray-300'
155 status === 'recording' ? 'border-red-400 bg-red-50 shadow-[0_0_20px_rgba(220,38,38,0.12)]' :
156 status === 'done' ? 'border-emerald-400 bg-emerald-50' :
157 'border-navy-200 bg-navy-50 hover:border-navy-300'
157158 }`}
158159 >
159160 {status === 'recording' ? (
160161 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#dc2626" strokeWidth="1.5"><path d="M2 12h2M6 8v8M10 5v14M14 9v6M18 7v10M22 12h-2"/></svg>
161162 ) : status === 'done' ? (
162 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#16a34a" strokeWidth="2"><path d="M5 13l4 4L19 7" strokeLinecap="round" strokeLinejoin="round"/></svg>
163 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#059669" strokeWidth="2"><path d="M5 13l4 4L19 7" strokeLinecap="round" strokeLinejoin="round"/></svg>
163164 ) : (
164 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#888" strokeWidth="1.5"><rect x="9" y="2" width="6" height="11" rx="3"/><path d="M5 10a7 7 0 0014 0"/><line x1="12" y1="21" x2="12" y2="17"/><line x1="9" y1="21" x2="15" y2="21"/></svg>
165 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#1e3554" strokeWidth="1.5"><rect x="9" y="2" width="6" height="11" rx="3"/><path d="M5 10a7 7 0 0014 0"/><line x1="12" y1="21" x2="12" y2="17"/><line x1="9" y1="21" x2="15" y2="21"/></svg>
165166 )}
166167 </button>
167 <span className={`text-[12px] ${
168 status === 'recording' ? 'text-red-500' : status === 'done' ? 'text-green-600' : 'text-gray-400'
168 <span className={`text-[12px] font-medium ${
169 status === 'recording' ? 'text-red-500' : status === 'done' ? 'text-emerald-600' : 'text-gray-400'
169170 }`}>
170171 {status === 'recording' ? 'Click to stop' : status === 'done' ? 'Done' : 'Click to try'}
171172 </span>
174175 </div>
175176
176177 {/* What this demo shows */}
177 <div className="max-w-lg mx-auto mb-12">
178 <h2 className="text-lg font-bold text-gray-900 mb-4 text-center">What this demo shows</h2>
178 <div className="max-w-lg mx-auto mb-14">
179 <h2 className="text-lg font-bold text-navy-900 mb-5 text-center">What this demo shows</h2>
179180 <div className="grid grid-cols-2 gap-3">
180181 {[
181182 { title: 'Voice punctuation', desc: 'Say "comma", "period", "new line" — they convert automatically' },
182183 { title: 'Auto-capitalization', desc: 'First letter + after periods get capitalized' },
183 { title: '60+ languages', desc: 'Switch language above. Full app supports real-time translation.' },
184 { title: '200+ languages', desc: 'Switch language above. Full app supports real-time translation.' },
184185 { title: 'Real-time', desc: 'Words appear as you speak, not after you stop' },
185186 ].map(f => (
186 <div key={f.title} className="p-4 rounded-xl border border-gray-100 bg-gray-50/50">
187 <h3 className="text-[12px] font-semibold text-gray-700 mb-1">{f.title}</h3>
188 <p className="text-[11px] text-gray-400 leading-relaxed">{f.desc}</p>
187 <div key={f.title} className="p-4 rounded-xl border border-gray-100 bg-[#FAFAF8]">
188 <h3 className="text-[12px] font-semibold text-navy-800 mb-1">{f.title}</h3>
189 <p className="text-[11px] text-gray-500 leading-relaxed">{f.desc}</p>
189190 </div>
190191 ))}
191192 </div>
193194
194195 {/* Want more? */}
195196 <div className="text-center">
196 <h2 className="text-lg font-bold text-gray-900 mb-2">Want AI grammar + rewrite?</h2>
197 <p className="text-[13px] text-gray-400 mb-4">This demo uses free browser speech. The full app adds AI grammar correction, tone adjustment, and 99%+ accuracy.</p>
198 <a href="/download" className="inline-block px-6 py-2.5 rounded-xl bg-indigo-600 text-white text-[13px] font-medium hover:bg-indigo-500 transition-all">
197 <h2 className="text-lg font-bold text-navy-900 mb-2">Want AI grammar + rewrite?</h2>
198 <p className="text-[13px] text-gray-500 mb-5">This demo uses free browser speech. The full app adds AI grammar correction, tone adjustment, and 99%+ accuracy.</p>
199 <Link href="/download" className="inline-block px-6 py-3 rounded-lg bg-navy-900 text-white text-[13px] font-semibold hover:bg-navy-800 transition-all">
199200 Download the Full App
200 </a>
201 </Link>
201202 </div>
202203 </div>
203204
Modifiedapp/page.jsx+5−4View fileUnifiedSplit
11'use client'
22
33import { useState, useRef, useEffect } from 'react'
4import Link from 'next/link'
45import Waveform from '../components/Waveform'
56import Nav from '../components/Nav'
67import Footer from '../components/Footer'
1415 label: 'Legal Writing',
1516 },
1617 {
17 before: 'Hey can u send me the report asap i need it for the meeting tmrw thx',
18 after: 'Hey, can you send me the report ASAP? I need it for the meeting tomorrow. Thanks!',
19 label: 'Email Polish',
18 before: 'hey can u send me the report i need it for the board meeting tmrw and make sure the numbers add up thx',
19 after: 'Hi, can you send me the report? I need it for the board meeting tomorrow. Please ensure the figures reconcile. Thanks.',
20 label: 'Professional Email',
2021 },
2122 {
2223 before: 'the total revenue was twelve million four hundred thousand dollars which is a increase of 8.3 percent year on year',
114115 </a>
115116 <a href="/live" className="btn-secondary text-base px-10">
116117 Try in Browser
117 </a>
118 </Link>
118119 </div>
119120
120121 {/* Demo Card — floating effect */}
Modifiedapp/pricing/PricingCards.jsx+20−19View fileUnifiedSplit
4747 return
4848 }
4949
50 // Redirect to Stripe's hosted checkout page
5150 window.location.href = data.url
5251 } catch {
5352 setMessage({ type: 'error', text: 'Network error. Check your connection and try again.' })
111110 badge: 'MOST POPULAR',
112111 features: [
113112 'Unlimited AI grammar corrections',
114 'Unlimited voice-to-text',
113 'Unlimited voice-to-text dictation',
115114 'AI Rewrite Mode (tone + polish)',
116115 'Preserve My Voice (learns your style)',
117116 'Context-aware (email, Slack, code)',
118117 'Desktop app (Mac + Windows)',
119118 'Chrome extension (all websites)',
120 'iPhone + Android keyboard (coming)',
119 'iPhone + Android keyboard',
121120 'Offline mode (privacy-first)',
122 '50+ languages',
123 'Custom vocabulary + macros',
121 'Real-time translation (200+ languages)',
122 'Custom vocabulary',
124123 ],
125124 cta: 'Start 7-Day Free Trial',
126125 action: () => handleCheckout('pro'),
151150 <>
152151 {message && (
153152 <div className={`mb-8 p-4 rounded-xl text-center text-[14px] font-medium ${
154 message.type === 'success' ? 'bg-green-50 text-green-700 border border-green-200' :
153 message.type === 'success' ? 'bg-emerald-50 text-emerald-700 border border-emerald-200' :
155154 message.type === 'error' ? 'bg-red-50 text-red-700 border border-red-200' :
156 'bg-blue-50 text-blue-700 border border-blue-200'
155 'bg-navy-50 text-navy-700 border border-navy-200'
157156 }`}>
158157 {message.text}
159158 </div>
161160
162161 <div className="grid md:grid-cols-3 gap-6">
163162 {plans.map((plan) => (
164 <div key={plan.name} className={`rounded-2xl p-6 flex flex-col border ${
165 plan.highlight ? 'border-indigo-200 bg-indigo-50/30 shadow-lg shadow-indigo-500/5 relative' : 'border-gray-200'
163 <div key={plan.name} className={`rounded-xl p-6 flex flex-col border ${
164 plan.highlight ? 'border-gold-300 bg-gold-50/15 shadow-lg shadow-gold-500/5 relative ring-1 ring-gold-200' : 'border-gray-200'
166165 }`}>
167166 {plan.badge && (
168 <span className="absolute -top-3 left-1/2 -translate-x-1/2 bg-indigo-600 text-white text-[10px] font-bold tracking-wider px-3 py-1 rounded-full">
167 <span className={`absolute -top-3 left-1/2 -translate-x-1/2 text-[10px] font-bold tracking-wider px-3 py-1 rounded-full ${
168 plan.name === 'Pro' ? 'bg-navy-900 text-white' : 'bg-gold-500 text-white'
169 }`}>
169170 {plan.badge}
170171 </span>
171172 )}
172 <h2 className="text-lg font-bold text-gray-800 mb-1">{plan.name}</h2>
173 <h2 className="text-lg font-bold text-navy-900 mb-1">{plan.name}</h2>
173174 <div className="mb-5">
174 <span className="text-4xl font-bold text-gray-900">{plan.price}</span>
175 <span className="text-4xl font-bold text-navy-900">{plan.price}</span>
175176 <span className="text-[13px] text-gray-400 ml-1">{plan.period}</span>
176177 </div>
177178 <ul className="flex-1 space-y-2.5 mb-6">
178179 {plan.features.map((f, i) => (
179 <li key={i} className="text-[13px] text-gray-500 flex items-start gap-2">
180 <svg className="w-4 h-4 text-indigo-500 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth="2"><path d="M5 13l4 4L19 7" strokeLinecap="round" strokeLinejoin="round"/></svg>
180 <li key={i} className="text-[13px] text-gray-600 flex items-start gap-2.5">
181 <svg className="w-4 h-4 text-emerald-500 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth="2"><path d="M5 13l4 4L19 7" strokeLinecap="round" strokeLinejoin="round"/></svg>
181182 {f}
182183 </li>
183184 ))}
185186 <button
186187 onClick={plan.action}
187188 disabled={loading === plan.planKey}
188 className={`block w-full text-center py-2.5 rounded-xl text-[13px] font-medium transition-all ${
189 className={`block w-full text-center py-3 rounded-lg text-[13px] font-semibold transition-all ${
189190 plan.highlight
190 ? 'bg-indigo-600 text-white hover:bg-indigo-500 disabled:bg-indigo-400'
191 : 'bg-gray-100 text-gray-600 hover:bg-gray-200 disabled:bg-gray-50'
191 ? 'bg-navy-900 text-white hover:bg-navy-800 disabled:bg-navy-600'
192 : 'bg-gray-100 text-gray-700 hover:bg-gray-200 disabled:bg-gray-50'
192193 }`}
193194 >
194195 {loading === plan.planKey ? 'Redirecting to checkout...' : plan.cta}
198199 </div>
199200
200201 {/* Manage subscription link for existing customers */}
201 <div className="text-center mt-6">
202 <div className="text-center mt-8">
202203 <button
203204 onClick={handleManage}
204205 disabled={loading === 'manage'}
205 className="text-[13px] text-gray-400 hover:text-indigo-600 transition-colors"
206 className="text-[13px] text-gray-400 hover:text-navy-700 transition-colors font-medium"
206207 >
207208 {loading === 'manage' ? 'Opening billing portal...' : 'Already subscribed? Manage your plan'}
208209 </button>
Modifiedapp/pricing/page.jsx+36−38View fileUnifiedSplit
33import PricingCards from './PricingCards'
44
55export const metadata = {
6 title: '48co Pricing — AI Grammar & Voice Plans | Free, Pro & Teams',
6 title: '48co Voice Pricing — AI Grammar & Dictation Plans for Professionals',
77 description: 'AI grammar correction + voice-to-text on every device. Free to start, Pro at $12/mo, Business $29/mo for 10 users. 60% cheaper than Grammarly.',
88 openGraph: {
9 title: '48co Pricing — AI Grammar That Works Everywhere',
9 title: '48co Voice Pricing — Professional AI Grammar & Dictation',
1010 description: 'Free grammar checks, $12/mo Pro, $29/mo Business for 10 users. 60% cheaper than Grammarly Premium.',
1111 },
1212}
1616 <main className="min-h-screen bg-white">
1717 <Nav />
1818
19 <div className="max-w-5xl mx-auto px-4 pt-28 pb-16">
19 <div className="max-w-5xl mx-auto px-4 pt-32 pb-16">
2020 <div className="text-center mb-16">
21 <h1 className="text-4xl font-bold text-gray-900 mb-3">Simple, honest pricing</h1>
22 <p className="text-gray-400 text-base max-w-md mx-auto">
23 Free to start. No credit card needed. Upgrade when you&apos;re ready.
21 <h1 className="text-4xl md:text-5xl font-bold text-navy-900 mb-4">Straightforward pricing</h1>
22 <p className="text-gray-500 text-base max-w-md mx-auto">
23 Free to start. No credit card needed. Upgrade when you&apos;re ready for unlimited AI.
2424 </p>
2525 </div>
2626
2727 {/* Business Highlight */}
28 <div className="mb-12 p-5 rounded-2xl bg-indigo-50 border border-indigo-100 text-center">
29 <p className="text-indigo-700 text-[15px] font-semibold mb-1">Business: $29/mo for your whole team (up to 10)</p>
28 <div className="mb-12 p-5 rounded-xl bg-navy-50 border border-navy-100 text-center">
29 <p className="text-navy-800 text-[15px] font-semibold mb-1">Business: $29/mo for your whole team (up to 10)</p>
3030 <p className="text-gray-500 text-[13px]">
3131 That&apos;s just $2.90 per user. Grammarly Business charges $15/user/mo — 5x more.
3232 </p>
3636 <PricingCards />
3737
3838 {/* vs Grammarly */}
39 <div className="mt-20 max-w-3xl mx-auto">
39 <div className="mt-24 max-w-3xl mx-auto">
4040 <div className="text-center mb-10">
41 <h2 className="text-2xl font-bold text-gray-900 mb-2">48co Pro vs Grammarly Premium</h2>
42 <p className="text-[14px] text-gray-400">Same job, better AI, lower price.</p>
41 <h2 className="text-2xl font-bold text-navy-900 mb-2">48co Pro vs Grammarly Premium</h2>
42 <p className="text-[14px] text-gray-500">Same job, better AI, lower price.</p>
4343 </div>
4444
45 <div className="border border-gray-200 rounded-2xl overflow-hidden">
45 <div className="border border-gray-200 rounded-xl overflow-hidden">
4646 <table className="w-full text-[13px]">
4747 <thead>
48 <tr className="border-b border-gray-100 bg-gray-50">
49 <th className="text-left py-3 px-5 text-gray-400 font-normal">Feature</th>
50 <th className="text-center py-3 px-5 text-indigo-600 font-semibold">48co Pro</th>
51 <th className="text-center py-3 px-5 text-gray-400 font-normal">Grammarly Premium</th>
48 <tr className="border-b border-gray-100 bg-[#FAFAF8]">
49 <th className="text-left py-3.5 px-5 text-gray-500 font-medium">Feature</th>
50 <th className="text-center py-3.5 px-5 text-navy-900 font-bold">48co Pro</th>
51 <th className="text-center py-3.5 px-5 text-gray-400 font-medium">Grammarly Premium</th>
5252 </tr>
5353 </thead>
5454 <tbody>
5858 { f: 'Business (10 users)', us: '$29/mo', them: '$150/mo' },
5959 { f: 'AI grammar correction', us: true, them: true },
6060 { f: 'Tone adjustment', us: true, them: true },
61 { f: 'Voice-to-text', us: true, them: false },
61 { f: 'Voice-to-text dictation', us: true, them: false },
6262 { f: 'AI Rewrite Mode', us: true, them: 'Limited' },
6363 { f: 'Context-aware (app detection)', us: true, them: false },
6464 { f: 'Desktop app (types into any app)', us: true, them: false },
65 { f: 'Offline mode', us: true, them: false },
66 { f: 'Real-time translation (60+ languages)', us: true, them: false },
67 { f: 'Real-time streaming (Deepgram)', us: true, them: false },
65 { f: 'Offline mode (privacy-first)', us: true, them: false },
66 { f: 'Real-time translation (200+ languages)', us: true, them: false },
6867 { f: 'iPhone & Android keyboard', us: true, them: true },
6968 { f: 'AI engine', us: 'Claude (latest)', them: 'Proprietary' },
70 { f: 'Developer mode (code fences)', us: true, them: false },
7169 ].map((row) => (
72 <tr key={row.f} className="border-b border-gray-50">
73 <td className="py-2.5 px-5 text-gray-500">{row.f}</td>
74 <td className="py-2.5 px-5 text-center font-medium">
75 {row.us === true ? <span className="text-green-600">Yes</span> :
70 <tr key={row.f} className="border-b border-gray-50 hover:bg-gray-50/50 transition-colors">
71 <td className="py-3 px-5 text-gray-600">{row.f}</td>
72 <td className="py-3 px-5 text-center font-medium">
73 {row.us === true ? <span className="text-emerald-600">Yes</span> :
7674 row.us === false ? <span className="text-gray-300">No</span> :
77 <span className="text-gray-700">{row.us}</span>}
75 <span className="text-navy-900">{row.us}</span>}
7876 </td>
79 <td className="py-2.5 px-5 text-center">
80 {row.them === true ? <span className="text-green-600">Yes</span> :
77 <td className="py-3 px-5 text-center">
78 {row.them === true ? <span className="text-emerald-600">Yes</span> :
8179 row.them === false ? <span className="text-gray-300">No</span> :
8280 <span className="text-gray-400">{row.them}</span>}
8381 </td>
8987 </div>
9088
9189 {/* FAQ */}
92 <div className="mt-20 max-w-2xl mx-auto">
93 <h2 className="text-2xl font-bold text-gray-900 mb-8 text-center">Frequently Asked Questions</h2>
94 <div className="space-y-6">
90 <div className="mt-24 max-w-2xl mx-auto">
91 <h2 className="text-2xl font-bold text-navy-900 mb-10 text-center">Frequently asked questions</h2>
92 <div className="space-y-8">
9593 {[
9694 { q: 'How does the free tier work?', a: 'You get 10 AI grammar corrections per day in the Chrome extension, plus 60 minutes of voice dictation per month. No credit card required. The free tier never expires.' },
97 { q: 'What does Pro include that Free doesn\'t?', a: 'Unlimited corrections, unlimited voice, AI Rewrite Mode (polishes your tone), context-aware formatting, desktop app, offline mode, real-time translation (60+ languages), mobile keyboard apps for iPhone and Android, and Deepgram real-time streaming.' },
98 { q: 'How is this better than Grammarly?', a: '48co uses Claude AI (the latest model) instead of rules-based checking. It also includes voice-to-text, works as a desktop app that types into ANY application (not just browsers), costs $12/mo vs $30/mo, and our Business plan is $29/mo for 10 users vs Grammarly\'s $150/mo.' },
99 { q: 'Do I need an API key?', a: 'No. All plans use our managed AI service. You just sign up and start using it.' },
100 { q: 'Will it work on my phone?', a: 'Yes. We have custom keyboard apps for both iPhone and Android. They work as a system-level keyboard in every app — texts, emails, notes, everything. Voice-to-text and grammar checking built right into the keyboard.' },
95 { q: 'What does Pro include that Free doesn\'t?', a: 'Unlimited corrections, unlimited voice, AI Rewrite Mode (polishes your tone), context-aware formatting, desktop app, offline mode, real-time translation (200+ languages), mobile keyboard apps, and Preserve My Voice (the AI learns your writing style).' },
96 { q: 'How is this better than Grammarly?', a: '48co uses Claude AI (the latest model) instead of rules-based checking. It includes voice-to-text, works as a desktop app that types into ANY application (not just browsers), costs $12/mo vs $30/mo, and our Business plan is $29/mo for 10 users vs Grammarly\'s $150/mo.' },
97 { q: 'Is my data secure?', a: 'Yes. You can run everything locally with our offline mode — zero data leaves your device. When using cloud features, all data is encrypted with TLS 1.3 and never stored on our servers. Designed for attorney-client privilege compliance.' },
98 { q: 'Will it work on my phone?', a: 'Yes. We have custom keyboard apps for both iPhone and Android. They work as a system-level keyboard in every app — texts, emails, notes, everything. Voice dictation and grammar checking built right into the keyboard.' },
10199 { q: 'Can I cancel anytime?', a: 'Yes, instantly. No contracts, no cancellation fees, no questions asked. Monthly plans cancel at the end of the billing period. Annual plans can be refunded within 14 days.' },
102100 ].map((faq) => (
103 <div key={faq.q}>
104 <h3 className="text-[14px] text-gray-800 font-semibold mb-1">{faq.q}</h3>
105 <p className="text-[13px] text-gray-400 leading-relaxed">{faq.a}</p>
101 <div key={faq.q} className="border-b border-gray-100 pb-6">
102 <h3 className="text-[15px] text-navy-900 font-semibold mb-2">{faq.q}</h3>
103 <p className="text-[13px] text-gray-500 leading-relaxed">{faq.a}</p>
106104 </div>
107105 ))}
108106 </div>
Modifiedapp/privacy/page.jsx+10−10View fileUnifiedSplit
1010 return (
1111 <main className="min-h-screen bg-white">
1212 <Nav />
13 <div className="max-w-3xl mx-auto px-6 pt-32 pb-20">
14 <h1 className="text-3xl font-bold text-gray-900 mb-2">Privacy Policy</h1>
15 <p className="text-sm text-gray-400 mb-12">Last updated: 29 March 2026</p>
13 <div className="max-w-3xl mx-auto px-6 pt-36 pb-20">
14 <h1 className="text-3xl font-bold text-navy-900 mb-2">Privacy Policy</h1>
15 <p className="text-sm text-gray-400 mb-14">Last updated: 29 March 2026</p>
1616
17 <div className="prose prose-gray prose-sm max-w-none [&_h2]:text-lg [&_h2]:font-semibold [&_h2]:text-gray-900 [&_h2]:mt-10 [&_h2]:mb-4 [&_p]:text-gray-600 [&_p]:leading-relaxed [&_p]:mb-4 [&_ul]:text-gray-600 [&_ul]:mb-4 [&_li]:mb-1">
17 <div className="prose prose-gray prose-sm max-w-none [&_h2]:text-lg [&_h2]:font-semibold [&_h2]:text-navy-900 [&_h2]:mt-10 [&_h2]:mb-4 [&_p]:text-gray-600 [&_p]:leading-relaxed [&_p]:mb-4 [&_ul]:text-gray-600 [&_ul]:mb-4 [&_li]:mb-1">
1818
1919 <h2>What We Collect</h2>
2020 <p>48co Voice collects only what is necessary to provide the service:</p>
4848
4949 <h2>Third-Party Services</h2>
5050 <ul className="list-disc pl-6">
51 <li><strong>Anthropic (Claude AI):</strong> Processes grammar checks and rewrites. Subject to <a href="https://www.anthropic.com/privacy" className="text-indigo-600 hover:text-indigo-800" target="_blank" rel="noopener">Anthropic&rsquo;s privacy policy</a>.</li>
52 <li><strong>OpenAI (Whisper):</strong> Processes voice transcription when you choose the Whisper engine. Subject to <a href="https://openai.com/privacy" className="text-indigo-600 hover:text-indigo-800" target="_blank" rel="noopener">OpenAI&rsquo;s privacy policy</a>.</li>
53 <li><strong>Stripe:</strong> Processes payments. We never see or store your card details. Subject to <a href="https://stripe.com/privacy" className="text-indigo-600 hover:text-indigo-800" target="_blank" rel="noopener">Stripe&rsquo;s privacy policy</a>.</li>
54 <li><strong>Google (Web Speech API):</strong> The free voice engine in the Chrome extension uses Google&rsquo;s Web Speech API. Audio is processed by Google. Subject to <a href="https://policies.google.com/privacy" className="text-indigo-600 hover:text-indigo-800" target="_blank" rel="noopener">Google&rsquo;s privacy policy</a>.</li>
51 <li><strong>Anthropic (Claude AI):</strong> Processes grammar checks and rewrites. Subject to <a href="https://www.anthropic.com/privacy" className="text-navy-700 hover:text-navy-900 font-medium" target="_blank" rel="noopener">Anthropic&rsquo;s privacy policy</a>.</li>
52 <li><strong>OpenAI (Whisper):</strong> Processes voice transcription when you choose the Whisper engine. Subject to <a href="https://openai.com/privacy" className="text-navy-700 hover:text-navy-900 font-medium" target="_blank" rel="noopener">OpenAI&rsquo;s privacy policy</a>.</li>
53 <li><strong>Stripe:</strong> Processes payments. We never see or store your card details. Subject to <a href="https://stripe.com/privacy" className="text-navy-700 hover:text-navy-900 font-medium" target="_blank" rel="noopener">Stripe&rsquo;s privacy policy</a>.</li>
54 <li><strong>Google (Web Speech API):</strong> The free voice engine in the Chrome extension uses Google&rsquo;s Web Speech API. Audio is processed by Google. Subject to <a href="https://policies.google.com/privacy" className="text-navy-700 hover:text-navy-900 font-medium" target="_blank" rel="noopener">Google&rsquo;s privacy policy</a>.</li>
5555 </ul>
5656
5757 <h2>Your Rights</h2>
58 <p>You can request deletion of your account and all associated data at any time by contacting <a href="mailto:support@48co.nz" className="text-indigo-600 hover:text-indigo-800">support@48co.nz</a>. We will delete your data within 30 days of receiving your request.</p>
58 <p>You can request deletion of your account and all associated data at any time by contacting <a href="mailto:support@48co.nz" className="text-navy-700 hover:text-navy-900 font-medium">support@48co.nz</a>. We will delete your data within 30 days of receiving your request.</p>
5959
6060 <h2>Changes to This Policy</h2>
6161 <p>We may update this policy from time to time. We will notify you of any material changes via email or a notice on our website.</p>
6262
6363 <h2>Contact</h2>
64 <p>For privacy questions or data requests, email <a href="mailto:support@48co.nz" className="text-indigo-600 hover:text-indigo-800">support@48co.nz</a>.</p>
64 <p>For privacy questions or data requests, email <a href="mailto:support@48co.nz" className="text-navy-700 hover:text-navy-900 font-medium">support@48co.nz</a>.</p>
6565 </div>
6666 </div>
6767 <Footer />
Modifiedapp/terms/page.jsx+5−5View fileUnifiedSplit
1010 return (
1111 <main className="min-h-screen bg-white">
1212 <Nav />
13 <div className="max-w-3xl mx-auto px-6 pt-32 pb-20">
14 <h1 className="text-3xl font-bold text-gray-900 mb-2">Terms of Service</h1>
15 <p className="text-sm text-gray-400 mb-12">Last updated: 29 March 2026</p>
13 <div className="max-w-3xl mx-auto px-6 pt-36 pb-20">
14 <h1 className="text-3xl font-bold text-navy-900 mb-2">Terms of Service</h1>
15 <p className="text-sm text-gray-400 mb-14">Last updated: 29 March 2026</p>
1616
17 <div className="prose prose-gray prose-sm max-w-none [&_h2]:text-lg [&_h2]:font-semibold [&_h2]:text-gray-900 [&_h2]:mt-10 [&_h2]:mb-4 [&_p]:text-gray-600 [&_p]:leading-relaxed [&_p]:mb-4 [&_ul]:text-gray-600 [&_ul]:mb-4 [&_li]:mb-1">
17 <div className="prose prose-gray prose-sm max-w-none [&_h2]:text-lg [&_h2]:font-semibold [&_h2]:text-navy-900 [&_h2]:mt-10 [&_h2]:mb-4 [&_p]:text-gray-600 [&_p]:leading-relaxed [&_p]:mb-4 [&_ul]:text-gray-600 [&_ul]:mb-4 [&_li]:mb-1">
1818
1919 <h2>Agreement</h2>
2020 <p>By using 48co Voice (&ldquo;the Service&rdquo;), you agree to these terms. If you do not agree, do not use the Service. The Service is provided by 48co Ltd, a New Zealand company.</p>
5757 <p>These terms are governed by the laws of New Zealand. Any disputes will be resolved in the courts of New Zealand.</p>
5858
5959 <h2>Contact</h2>
60 <p>Questions about these terms? Email <a href="mailto:support@48co.nz" className="text-indigo-600 hover:text-indigo-800">support@48co.nz</a>.</p>
60 <p>Questions about these terms? Email <a href="mailto:support@48co.nz" className="text-navy-700 hover:text-navy-900 font-medium">support@48co.nz</a>.</p>
6161 </div>
6262 </div>
6363 <Footer />
Modifiedcomponents/Footer.jsx+30−23View fileUnifiedSplit
1import Link from 'next/link'
2
13export default function Footer() {
24 return (
3 <footer className="border-t border-black/[0.06] py-12 bg-white">
4 <div className="max-w-5xl mx-auto px-6">
5 <div className="flex flex-col md:flex-row items-start justify-between gap-10">
6 <div>
7 <span className="text-[15px] font-bold text-gray-800">48<span className="text-indigo-600">co</span></span>
8 <p className="text-[12px] text-gray-400 mt-1.5">AI grammar & voice-to-text for professionals.</p>
9 <p className="text-[11px] text-gray-300 mt-3">Built in New Zealand</p>
5 <footer className="bg-navy-950 text-white">
6 <div className="max-w-6xl mx-auto px-6 py-16">
7 <div className="flex flex-col md:flex-row items-start justify-between gap-12">
8 <div className="max-w-xs">
9 <span className="text-[17px] font-bold tracking-tight">
10 48<span className="text-gold-400">co</span>
11 <span className="text-[10px] font-medium text-white/40 tracking-widest uppercase ml-2">Voice</span>
12 </span>
13 <p className="text-[13px] text-white/50 mt-3 leading-relaxed">
14 AI grammar, voice-to-text, and translation for legal, accounting, and medical professionals.
15 </p>
16 <p className="text-[11px] text-white/30 mt-4">Built in New Zealand</p>
1017 </div>
1118
12 <div className="flex gap-16">
19 <div className="flex gap-20">
1320 <div>
14 <p className="text-[11px] font-semibold text-gray-500 uppercase tracking-wide mb-3">Product</p>
15 <div className="flex flex-col gap-2 text-[12px] text-gray-400">
16 <a href="/download" className="hover:text-gray-700 transition-colors">Download</a>
17 <a href="/install" className="hover:text-gray-700 transition-colors">Chrome Extension</a>
18 <a href="/live" className="hover:text-gray-700 transition-colors">Try Live</a>
19 <a href="/compare" className="hover:text-gray-700 transition-colors">Compare</a>
21 <p className="text-[11px] font-semibold text-white/40 uppercase tracking-widest mb-4">Product</p>
22 <div className="flex flex-col gap-3 text-[13px] text-white/50">
23 <Link href="/download" className="hover:text-white transition-colors">Download</Link>
24 <Link href="/install" className="hover:text-white transition-colors">Chrome Extension</Link>
25 <Link href="/live" className="hover:text-white transition-colors">Try Live</Link>
26 <Link href="/compare" className="hover:text-white transition-colors">Compare</Link>
2027 </div>
2128 </div>
2229 <div>
23 <p className="text-[11px] font-semibold text-gray-500 uppercase tracking-wide mb-3">Company</p>
24 <div className="flex flex-col gap-2 text-[12px] text-gray-400">
25 <a href="/pricing" className="hover:text-gray-700 transition-colors">Pricing</a>
26 <a href="/privacy" className="hover:text-gray-700 transition-colors">Privacy Policy</a>
27 <a href="/terms" className="hover:text-gray-700 transition-colors">Terms of Service</a>
28 <a href="mailto:support@48co.nz" className="hover:text-gray-700 transition-colors">Support</a>
30 <p className="text-[11px] font-semibold text-white/40 uppercase tracking-widest mb-4">Company</p>
31 <div className="flex flex-col gap-3 text-[13px] text-white/50">
32 <Link href="/pricing" className="hover:text-white transition-colors">Pricing</Link>
33 <Link href="/privacy" className="hover:text-white transition-colors">Privacy Policy</Link>
34 <Link href="/terms" className="hover:text-white transition-colors">Terms of Service</Link>
35 <a href="mailto:support@48co.nz" className="hover:text-white transition-colors">Support</a>
2936 </div>
3037 </div>
3138 </div>
3239 </div>
3340
34 <div className="border-t border-black/[0.04] mt-8 pt-6 flex flex-col sm:flex-row items-center justify-between gap-4">
35 <p className="text-[11px] text-gray-300">&copy; {new Date().getFullYear()} 48co Ltd. All rights reserved.</p>
36 <p className="text-[11px] text-gray-300">Your data is encrypted and never shared with third parties.</p>
41 <div className="border-t border-white/[0.06] mt-12 pt-8 flex flex-col sm:flex-row items-center justify-between gap-4">
42 <p className="text-[11px] text-white/30">&copy; {new Date().getFullYear()} 48co Ltd. All rights reserved.</p>
43 <p className="text-[11px] text-white/30">Your data is encrypted and never shared with third parties.</p>
3744 </div>
3845 </div>
3946 </footer>
Modifiedcomponents/Nav.jsx+26−22View fileUnifiedSplit
11'use client'
22
33import { useState } from 'react'
4import Link from 'next/link'
45
56export default function Nav() {
67 const [open, setOpen] = useState(false)
78
89 return (
9 <nav className="fixed top-0 w-full z-50 bg-white/80 backdrop-blur-xl border-b border-black/[0.04]">
10 <div className="max-w-6xl mx-auto flex items-center justify-between px-6 py-3">
11 <a href="/" className="text-base font-bold tracking-tight">
12 48<span className="text-indigo-600">co</span>
13 </a>
10 <nav className="fixed top-0 w-full z-50 bg-white/90 backdrop-blur-xl border-b border-black/[0.04]">
11 <div className="max-w-6xl mx-auto flex items-center justify-between px-6 py-4">
12 <Link href="/" className="flex items-center gap-2">
13 <span className="text-[17px] font-bold tracking-tight text-navy-900">
14 48<span className="text-gold-500">co</span>
15 </span>
16 <span className="text-[10px] font-medium text-gray-400 tracking-widest uppercase hidden sm:inline">Voice</span>
17 </Link>
1418
1519 {/* Desktop nav */}
16 <div className="hidden sm:flex items-center gap-6">
17 <a href="/compare" className="text-[13px] text-gray-400 hover:text-gray-700 transition-colors">Compare</a>
18 <a href="/pricing" className="text-[13px] text-gray-400 hover:text-gray-700 transition-colors">Pricing</a>
19 <a href="/live" className="text-[13px] text-gray-400 hover:text-gray-700 transition-colors">Try Live</a>
20 <a href="/download" className="text-[13px] px-4 py-1.5 rounded-lg bg-indigo-600 text-white hover:bg-indigo-500 transition-all">
20 <div className="hidden md:flex items-center gap-8">
21 <Link href="/compare" className="text-[13px] text-gray-500 hover:text-navy-900 transition-colors font-medium">Compare</Link>
22 <Link href="/pricing" className="text-[13px] text-gray-500 hover:text-navy-900 transition-colors font-medium">Pricing</Link>
23 <Link href="/live" className="text-[13px] text-gray-500 hover:text-navy-900 transition-colors font-medium">Try Live</Link>
24 <Link href="/download" className="text-[13px] px-5 py-2 rounded-lg bg-navy-900 text-white hover:bg-navy-800 transition-all font-medium">
2125 Download
22 </a>
26 </Link>
2327 </div>
2428
2529 {/* Mobile hamburger */}
2630 <button
2731 onClick={() => setOpen(!open)}
28 className="sm:hidden flex flex-col justify-center items-center w-8 h-8 gap-[5px]"
32 className="md:hidden flex flex-col justify-center items-center w-8 h-8 gap-[5px]"
2933 aria-label="Toggle menu"
3034 >
31 <span className={`block w-5 h-[1.5px] bg-gray-600 transition-all duration-200 ${open ? 'rotate-45 translate-y-[6.5px]' : ''}`} />
32 <span className={`block w-5 h-[1.5px] bg-gray-600 transition-all duration-200 ${open ? 'opacity-0' : ''}`} />
33 <span className={`block w-5 h-[1.5px] bg-gray-600 transition-all duration-200 ${open ? '-rotate-45 -translate-y-[6.5px]' : ''}`} />
35 <span className={`block w-5 h-[1.5px] bg-navy-900 transition-all duration-200 ${open ? 'rotate-45 translate-y-[6.5px]' : ''}`} />
36 <span className={`block w-5 h-[1.5px] bg-navy-900 transition-all duration-200 ${open ? 'opacity-0' : ''}`} />
37 <span className={`block w-5 h-[1.5px] bg-navy-900 transition-all duration-200 ${open ? '-rotate-45 -translate-y-[6.5px]' : ''}`} />
3438 </button>
3539 </div>
3640
3741 {/* Mobile menu */}
3842 {open && (
39 <div className="sm:hidden bg-white border-t border-black/[0.04] px-6 py-4 flex flex-col gap-3">
40 <a href="/compare" className="text-[14px] text-gray-500 hover:text-gray-900 transition-colors py-1">Compare</a>
41 <a href="/pricing" className="text-[14px] text-gray-500 hover:text-gray-900 transition-colors py-1">Pricing</a>
42 <a href="/live" className="text-[14px] text-gray-500 hover:text-gray-900 transition-colors py-1">Try Live</a>
43 <a href="/install" className="text-[14px] text-gray-500 hover:text-gray-900 transition-colors py-1">Chrome Extension</a>
44 <a href="/download" className="text-[14px] text-center px-4 py-2.5 rounded-lg bg-indigo-600 text-white hover:bg-indigo-500 transition-all mt-1">
43 <div className="md:hidden bg-white border-t border-black/[0.04] px-6 py-5 flex flex-col gap-4">
44 <Link href="/compare" className="text-[14px] text-gray-600 hover:text-navy-900 transition-colors font-medium py-1">Compare</Link>
45 <Link href="/pricing" className="text-[14px] text-gray-600 hover:text-navy-900 transition-colors font-medium py-1">Pricing</Link>
46 <Link href="/live" className="text-[14px] text-gray-600 hover:text-navy-900 transition-colors font-medium py-1">Try Live</Link>
47 <Link href="/install" className="text-[14px] text-gray-600 hover:text-navy-900 transition-colors font-medium py-1">Chrome Extension</Link>
48 <Link href="/download" className="text-[14px] text-center px-5 py-2.5 rounded-lg bg-navy-900 text-white hover:bg-navy-800 transition-all font-medium mt-2">
4549 Download
46 </a>
50 </Link>
4751 </div>
4852 )}
4953 </nav>
Modifiedcomponents/Waveform.jsx+2−3View fileUnifiedSplit
1616 key={i}
1717 className={`w-[3px] rounded-full transition-all duration-300 ${
1818 isRecording
19 ? `${cls} bg-indigo-500`
20 : 'bg-white/20'
19 ? `${cls} bg-gold-400`
20 : 'bg-white/10'
2121 }`}
22 // Static bars have varying heights for visual interest when idle
2322 style={isRecording ? {} : { height: `${4 + (i % 5) * 3}px` }}
2423 />
2524 ))}
Addeddown-scanner/.gitignore+3−0View fileUnifiedSplit
1/target
2Cargo.lock
3*.pdb
Addeddown-scanner/Cargo.toml+37−0View fileUnifiedSplit
1[package]
2name = "down"
3version = "0.2.0"
4edition = "2021"
5description = "DOWN — Personal AI-powered Windows security scanner"
6license = "MIT"
7
8[[bin]]
9name = "down"
10path = "src/main.rs"
11
12[dependencies]
13clap = { version = "4.5", features = ["derive"] }
14sysinfo = "0.33"
15walkdir = "2.5"
16sha2 = "0.10"
17colored = "3.0"
18chrono = "0.4"
19serde = { version = "1.0", features = ["derive"] }
20serde_json = "1.0"
21dirs = "6.0"
22ureq = "3.0"
23
24[target.'cfg(windows)'.dependencies]
25winreg = "0.55"
26windows = { version = "0.61", features = [
27 "Win32_NetworkManagement_IpHelper",
28 "Win32_Networking_WinSock",
29 "Win32_System_ProcessStatus",
30 "Win32_Security",
31] }
32
33[profile.release]
34strip = true
35lto = true
36codegen-units = 1
37opt-level = "z"
Addeddown-scanner/README.md+56−0View fileUnifiedSplit
1# DOWN — Windows Security Scanner
2
3A lightweight, fast Windows security scanner built in Rust. Detects and removes malware, scareware, and potentially unwanted programs (PUPs).
4
5**Under 1MB.** No bloat. No telemetry. No subscriptions.
6
7## What It Scans
8
9| Module | What It Checks |
10|--------|---------------|
11| **Processes** | Running processes against known malware names, suspicious paths, cryptominer CPU usage |
12| **Startup** | Registry Run keys, startup folders, scheduled tasks for persistence |
13| **Files** | Downloads, Temp, AppData for known malware hashes, double extensions (e.g. `invoice.pdf.exe`) |
14| **Browser Extensions** | Chrome, Edge, Firefox extensions for known malicious IDs and excessive permissions |
15| **Network** | Hosts file tampering, suspicious DNS servers, connections to known bad IPs |
16| **Scareware** | Fake antivirus, fake optimizers, fake cleaners — programs that scare you into paying |
17
18## Usage
19
20```
21down.exe --scan # Full scan (all modules)
22down.exe --quick # Quick scan (processes + startup only)
23down.exe --quarantine # Scan and remove all threats found
24down.exe --list-quarantine # Show quarantined items
25down.exe --restore <ID> # Restore a false positive
26```
27
28## Build From Source
29
30Requires [Rust](https://rustup.rs/).
31
32```bash
33cargo build --release
34```
35
36Binary appears at `target/release/down.exe` (Windows) or `target/release/down` (Linux/Mac for testing).
37
38### Cross-compile for Windows from Linux:
39
40```bash
41rustup target add x86_64-pc-windows-gnu
42sudo apt install mingw-w64
43cargo build --release --target x86_64-pc-windows-gnu
44```
45
46## How Quarantine Works
47
48Detected threats are moved to a quarantine folder (`%LOCALAPPDATA%\DownScanner\quarantine\` on Windows). A manifest tracks where each file came from so you can restore false positives with `--restore <ID>`.
49
50## Scan Logs
51
52Every scan writes a log to `%LOCALAPPDATA%\DownScanner\logs\` (Windows) or `~/.down-scanner/logs/` (Linux/Mac).
53
54## License
55
56MIT
Addeddown-scanner/src/elevation.rs+104−0View fileUnifiedSplit
1use colored::*;
2
3/// Check if the current process is running with admin privileges
4pub fn is_admin() -> bool {
5 #[cfg(windows)]
6 {
7 // Check if we're in the Administrators group
8 use std::process::Command;
9 let output = Command::new("net").args(["session"]).output();
10 match output {
11 Ok(o) => o.status.success(),
12 Err(_) => false,
13 }
14 }
15
16 #[cfg(not(windows))]
17 {
18 // On Linux/Mac, check if we're root
19 libc_geteuid() == 0
20 }
21}
22
23#[cfg(not(windows))]
24fn libc_geteuid() -> u32 {
25 // Simple check — if we can write to /etc, we're probably root
26 std::fs::metadata("/etc/shadow")
27 .map(|_| 0u32)
28 .unwrap_or(1000)
29}
30
31/// Request elevation and re-run the current process as admin.
32/// Returns Ok(true) if elevation was launched (caller should exit).
33/// Returns Ok(false) if already admin.
34/// Returns Err if elevation failed.
35pub fn request_elevation() -> Result<bool, String> {
36 if is_admin() {
37 return Ok(false);
38 }
39
40 println!(
41 "\n {} {}",
42 "[!]".yellow().bold(),
43 "This action requires administrator privileges.".yellow()
44 );
45
46 #[cfg(windows)]
47 {
48 println!(
49 " {} A Windows elevation prompt will appear...\n",
50 "[i]".blue()
51 );
52
53 let exe = std::env::current_exe()
54 .map_err(|e| format!("Failed to get executable path: {}", e))?;
55
56 let args: Vec<String> = std::env::args().skip(1).collect();
57 let args_str = args.join(" ");
58
59 // Use PowerShell Start-Process with -Verb RunAs for UAC elevation
60 let status = std::process::Command::new("powershell")
61 .args([
62 "-Command",
63 &format!(
64 "Start-Process -FilePath '{}' -ArgumentList '{}' -Verb RunAs -Wait",
65 exe.display(),
66 args_str
67 ),
68 ])
69 .status()
70 .map_err(|e| format!("Failed to request elevation: {}", e))?;
71
72 if status.success() {
73 Ok(true) // Elevated process was launched
74 } else {
75 Err("Elevation was denied or failed. Please right-click and Run as Administrator.".to_string())
76 }
77 }
78
79 #[cfg(not(windows))]
80 {
81 println!(
82 " {} Run with: sudo {}\n",
83 "[i]".blue(),
84 std::env::args().collect::<Vec<_>>().join(" ")
85 );
86 Err("Please re-run with sudo for full functionality.".to_string())
87 }
88}
89
90/// Print a warning if not running as admin (non-blocking)
91pub fn warn_if_not_admin() {
92 if !is_admin() {
93 println!(
94 " {} {}",
95 "[!]".yellow(),
96 "Running without admin privileges. Some checks will be limited.".dimmed()
97 );
98 println!(
99 " {} {}\n",
100 "[i]".blue(),
101 "Run as Administrator for full scan + removal capabilities.".dimmed()
102 );
103 }
104}
Addeddown-scanner/src/main.rs+308−0View fileUnifiedSplit
1mod browser_fix;
2mod elevation;
3mod quarantine;
4mod remover;
5mod report;
6mod scanner;
7mod signatures;
8mod threat;
9mod updater;
10
11use clap::Parser;
12use colored::*;
13use std::time::Instant;
14use sysinfo::System;
15
16#[derive(Parser)]
17#[command(
18 name = "down",
19 about = "DOWN — Personal AI-powered Windows Security Scanner v0.2",
20 long_about = "Scans your Windows PC for malware, scareware, and potentially unwanted programs.\nBuilt in Rust for speed and safety. No telemetry, no cloud dependency.\n\nv0.2: Nuke mode, browser fix, Defender protection, signature updates.",
21 version
22)]
23struct Cli {
24 /// Run a full scan (all modules) — default if no flags
25 #[arg(long, default_value_t = false)]
26 scan: bool,
27
28 /// Run a quick scan (processes + startup only)
29 #[arg(long, default_value_t = false)]
30 quick: bool,
31
32 /// NUKE MODE: Scan + aggressively remove ALL threats (uninstall, delete, kill)
33 #[arg(long, default_value_t = false)]
34 nuke: bool,
35
36 /// Fix browser hijacking (reset homepage, search engine, remove bad extensions)
37 #[arg(long, default_value_t = false)]
38 fix_browser: bool,
39
40 /// Quarantine detected threats (move to safe folder)
41 #[arg(long, default_value_t = false)]
42 quarantine: bool,
43
44 /// Restore a quarantined file by ID
45 #[arg(long, value_name = "ID")]
46 restore: Option<usize>,
47
48 /// List all quarantined items
49 #[arg(long, default_value_t = false)]
50 list_quarantine: bool,
51
52 /// Download latest threat signatures
53 #[arg(long, default_value_t = false)]
54 update_sigs: bool,
55}
56
57fn main() {
58 let cli = Cli::parse();
59
60 // Handle non-scan commands first
61 if let Some(id) = cli.restore {
62 report::print_banner();
63 println!(
64 "{} Restoring quarantined item #{}...\n",
65 "[*]".cyan().bold(),
66 id
67 );
68 match quarantine::restore_file(id) {
69 Ok(_) => println!("\n {} Done.", "[✓]".green().bold()),
70 Err(e) => println!("\n {} {}", "[✗]".red().bold(), e),
71 }
72 return;
73 }
74
75 if cli.list_quarantine {
76 report::print_banner();
77 quarantine::list_quarantine();
78 return;
79 }
80
81 if cli.update_sigs {
82 report::print_banner();
83 match updater::update_signatures() {
84 Ok(_) => println!("\n {} Signatures are up to date.", "[✓]".green().bold()),
85 Err(e) => println!("\n {} {}", "[!]".yellow(), e),
86 }
87 return;
88 }
89
90 if cli.fix_browser {
91 report::print_banner();
92 let fixed = browser_fix::fix_all_browsers();
93 println!(
94 "\n {} Fixed {} browser profile(s).",
95 "[✓]".green().bold(),
96 fixed
97 );
98 return;
99 }
100
101 // For nuke mode, request elevation if needed
102 if cli.nuke {
103 report::print_banner();
104 if !elevation::is_admin() {
105 match elevation::request_elevation() {
106 Ok(true) => return, // Elevated process was launched
107 Ok(false) => {} // Already admin (shouldn't reach here)
108 Err(e) => {
109 println!(" {} {}", "[!]".yellow(), e);
110 println!(" {} Continuing without admin — some removals may fail.\n", "[i]".blue());
111 }
112 }
113 }
114 let threats = run_full_scan();
115 handle_nuke(&threats);
116 return;
117 }
118
119 // Default scan modes
120 let is_full = cli.scan || (!cli.quick && !cli.quarantine);
121 let is_quick = cli.quick;
122
123 report::print_banner();
124 elevation::warn_if_not_admin();
125
126 if is_full {
127 let threats = run_full_scan();
128 handle_results(&threats, cli.quarantine);
129 } else if is_quick {
130 let threats = run_quick_scan();
131 handle_results(&threats, cli.quarantine);
132 } else if cli.quarantine {
133 println!(
134 " {} Running scan before quarantine...\n",
135 "[i]".blue()
136 );
137 let threats = run_full_scan();
138 handle_results(&threats, true);
139 }
140}
141
142fn run_full_scan() -> Vec<threat::Threat> {
143 report::print_scan_start("Full System Scan");
144 let start = Instant::now();
145 let mut all_threats = Vec::new();
146
147 // 1. Process scan
148 report::print_module_start("Scanning running processes...");
149 let mut system = System::new_all();
150 system.refresh_all();
151 let proc_threats = scanner::processes::scan(&system);
152 report_module_results("Processes", &proc_threats);
153 all_threats.extend(proc_threats);
154
155 // 2. Startup scan
156 report::print_module_start("Scanning startup entries...");
157 let startup_threats = scanner::startup::scan();
158 report_module_results("Startup entries", &startup_threats);
159 all_threats.extend(startup_threats);
160
161 // 3. File scan
162 report::print_module_start("Scanning file system...");
163 let file_threats = scanner::files::scan();
164 report_module_results("File system", &file_threats);
165 all_threats.extend(file_threats);
166
167 // 4. Browser extension scan
168 report::print_module_start("Auditing browser extensions...");
169 let browser_threats = scanner::browser::scan();
170 report_module_results("Browser extensions", &browser_threats);
171 all_threats.extend(browser_threats);
172
173 // 5. Network scan
174 report::print_module_start("Checking network configuration...");
175 let net_threats = scanner::network::scan();
176 report_module_results("Network configuration", &net_threats);
177 all_threats.extend(net_threats);
178
179 // 6. Scareware scan (now includes Defender tampering + proxy)
180 report::print_module_start("Scanning for scareware, PUPs & Defender tampering...");
181 let scare_threats = scanner::scareware::scan();
182 report_module_results("Scareware / PUPs / Defender", &scare_threats);
183 all_threats.extend(scare_threats);
184
185 let elapsed = start.elapsed();
186 println!(
187 "\n {} Scan completed in {:.1}s",
188 "[i]".blue(),
189 elapsed.as_secs_f64()
190 );
191
192 all_threats
193}
194
195fn run_quick_scan() -> Vec<threat::Threat> {
196 report::print_scan_start("Quick Scan (Processes + Startup)");
197 let start = Instant::now();
198 let mut all_threats = Vec::new();
199
200 report::print_module_start("Scanning running processes...");
201 let mut system = System::new_all();
202 system.refresh_all();
203 let proc_threats = scanner::processes::scan(&system);
204 report_module_results("Processes", &proc_threats);
205 all_threats.extend(proc_threats);
206
207 report::print_module_start("Scanning startup entries...");
208 let startup_threats = scanner::startup::scan();
209 report_module_results("Startup entries", &startup_threats);
210 all_threats.extend(startup_threats);
211
212 let elapsed = start.elapsed();
213 println!(
214 "\n {} Quick scan completed in {:.1}s",
215 "[i]".blue(),
216 elapsed.as_secs_f64()
217 );
218
219 all_threats
220}
221
222fn report_module_results(module: &str, threats: &[threat::Threat]) {
223 if threats.is_empty() {
224 report::print_module_clean(module);
225 } else {
226 for t in threats {
227 report::print_threat(t);
228 }
229 }
230}
231
232fn handle_results(threats: &[threat::Threat], do_quarantine: bool) {
233 let mut sorted = threats.to_vec();
234 sorted.sort_by(|a, b| b.severity.cmp(&a.severity));
235
236 report::print_summary(&sorted);
237
238 if let Err(e) = report::write_log(&sorted) {
239 println!(" {} Failed to write log: {}", "[!]".red(), e);
240 }
241
242 if do_quarantine && !sorted.is_empty() {
243 println!(
244 "\n{} {}",
245 "[*]".cyan().bold(),
246 "Quarantining threats...".yellow().bold()
247 );
248 match quarantine::quarantine_threats(&sorted) {
249 Ok(count) => {
250 println!(
251 "\n {} Quarantined {} items.",
252 "[✓]".green().bold(),
253 count
254 );
255 }
256 Err(e) => {
257 println!("\n {} Quarantine error: {}", "[✗]".red().bold(), e);
258 }
259 }
260 } else if !sorted.is_empty() {
261 println!(
262 " {} Use {} to remove threats, or {} for aggressive removal.",
263 "[i]".blue(),
264 "down --quarantine".yellow(),
265 "down --nuke".red().bold()
266 );
267 }
268}
269
270fn handle_nuke(threats: &[threat::Threat]) {
271 let mut sorted = threats.to_vec();
272 sorted.sort_by(|a, b| b.severity.cmp(&a.severity));
273
274 report::print_summary(&sorted);
275
276 if let Err(e) = report::write_log(&sorted) {
277 println!(" {} Failed to write log: {}", "[!]".red(), e);
278 }
279
280 if sorted.is_empty() {
281 return;
282 }
283
284 println!(
285 "\n{} {}",
286 "[*]".red().bold(),
287 "NUKE MODE: Removing all threats...".red().bold()
288 );
289 println!("{}", "─".repeat(60).red());
290
291 match remover::nuke_threats(&sorted) {
292 Ok(count) => {
293 println!(
294 "\n {} Removed {} threats.",
295 "[✓]".green().bold(),
296 count
297 );
298 println!(
299 " {} Run {} again to verify clean.",
300 "[i]".blue(),
301 "down --scan".yellow()
302 );
303 }
304 Err(e) => {
305 println!("\n {} Removal error: {}", "[✗]".red().bold(), e);
306 }
307 }
308}
Addeddown-scanner/src/quarantine.rs+348−0View fileUnifiedSplit
1use crate::threat::{Threat, ThreatAction};
2use chrono::Local;
3use colored::*;
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::PathBuf;
7
8#[derive(Debug, Serialize, Deserialize)]
9pub struct QuarantineEntry {
10 pub id: usize,
11 pub original_path: String,
12 pub quarantine_path: String,
13 pub threat_name: String,
14 pub quarantined_at: String,
15}
16
17#[derive(Debug, Serialize, Deserialize, Default)]
18pub struct QuarantineManifest {
19 pub entries: Vec<QuarantineEntry>,
20 pub next_id: usize,
21}
22
23impl QuarantineManifest {
24 pub fn load() -> Self {
25 let manifest_path = get_quarantine_dir().join("manifest.json");
26 if manifest_path.exists() {
27 let content = fs::read_to_string(&manifest_path).unwrap_or_default();
28 serde_json::from_str(&content).unwrap_or_default()
29 } else {
30 QuarantineManifest {
31 entries: Vec::new(),
32 next_id: 1,
33 }
34 }
35 }
36
37 pub fn save(&self) -> Result<(), String> {
38 let quarantine_dir = get_quarantine_dir();
39 fs::create_dir_all(&quarantine_dir)
40 .map_err(|e| format!("Failed to create quarantine directory: {}", e))?;
41
42 let manifest_path = quarantine_dir.join("manifest.json");
43 let content = serde_json::to_string_pretty(self)
44 .map_err(|e| format!("Failed to serialize manifest: {}", e))?;
45
46 fs::write(&manifest_path, content)
47 .map_err(|e| format!("Failed to write manifest: {}", e))?;
48
49 Ok(())
50 }
51}
52
53/// Execute quarantine actions for all threats
54pub fn quarantine_threats(threats: &[Threat]) -> Result<usize, String> {
55 let mut manifest = QuarantineManifest::load();
56 let quarantine_dir = get_quarantine_dir();
57 fs::create_dir_all(&quarantine_dir)
58 .map_err(|e| format!("Failed to create quarantine directory: {}", e))?;
59
60 let mut quarantined_count = 0;
61
62 for threat in threats {
63 match &threat.action {
64 ThreatAction::KillProcess(pid) => {
65 #[cfg(windows)]
66 {
67 let result = std::process::Command::new("taskkill")
68 .args(["/F", "/PID", &pid.to_string()])
69 .output();
70
71 match result {
72 Ok(output) if output.status.success() => {
73 println!(
74 " {} Killed process PID {}",
75 "[\u{2713}]".green().bold(),
76 pid
77 );
78 quarantined_count += 1;
79 }
80 _ => {
81 println!(
82 " {} Failed to kill PID {} (may need admin privileges)",
83 "[\u{2717}]".red(),
84 pid
85 );
86 }
87 }
88 }
89
90 #[cfg(not(windows))]
91 {
92 println!(
93 " {} Would kill PID {} (Windows only)",
94 "[~]".yellow(),
95 pid
96 );
97 }
98 }
99
100 ThreatAction::QuarantineFile(path) => {
101 let source = PathBuf::from(path);
102 if !source.exists() {
103 println!(
104 " {} File not found (already removed?): {}",
105 "[~]".yellow(),
106 path
107 );
108 continue;
109 }
110
111 let id = manifest.next_id;
112 let dest_name = format!("quarantine_{}", id);
113 let dest = quarantine_dir.join(&dest_name);
114
115 match fs::rename(&source, &dest) {
116 Ok(_) => {
117 manifest.entries.push(QuarantineEntry {
118 id,
119 original_path: path.clone(),
120 quarantine_path: dest.to_string_lossy().to_string(),
121 threat_name: threat.name.clone(),
122 quarantined_at: Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
123 });
124 manifest.next_id += 1;
125 quarantined_count += 1;
126
127 println!(
128 " {} Quarantined: {} (ID: {})",
129 "[\u{2713}]".green().bold(),
130 path,
131 id
132 );
133 }
134 Err(e) => {
135 if fs::copy(&source, &dest).is_ok() && fs::remove_file(&source).is_ok() {
136 manifest.entries.push(QuarantineEntry {
137 id,
138 original_path: path.clone(),
139 quarantine_path: dest.to_string_lossy().to_string(),
140 threat_name: threat.name.clone(),
141 quarantined_at: Local::now()
142 .format("%Y-%m-%d %H:%M:%S")
143 .to_string(),
144 });
145 manifest.next_id += 1;
146 quarantined_count += 1;
147
148 println!(
149 " {} Quarantined: {} (ID: {})",
150 "[\u{2713}]".green().bold(),
151 path,
152 id
153 );
154 } else {
155 println!(
156 " {} Failed to quarantine: {} \u{2014} {}",
157 "[\u{2717}]".red(),
158 path,
159 e
160 );
161 }
162 }
163 }
164 }
165
166 ThreatAction::RemoveStartupEntry {
167 key_path: _,
168 value_name: _,
169 } => {
170 #[cfg(windows)]
171 {
172 remove_registry_entry(_key_path, _value_name, &mut quarantined_count);
173 }
174
175 #[cfg(not(windows))]
176 {
177 println!(
178 " {} Would remove startup entry (Windows only)",
179 "[~]".yellow()
180 );
181 }
182 }
183
184 ThreatAction::ManualReview => {
185 println!(
186 " {} Skipped (manual review needed): {}",
187 "[~]".yellow(),
188 threat.name
189 );
190 }
191 }
192 }
193
194 manifest.save()?;
195 Ok(quarantined_count)
196}
197
198#[cfg(windows)]
199fn remove_registry_entry(key_path: &str, value_name: &str, count: &mut usize) {
200 use winreg::enums::*;
201 use winreg::RegKey;
202
203 let (hive, subkey) = if key_path.starts_with("HKCU") {
204 (
205 RegKey::predef(HKEY_CURRENT_USER),
206 key_path.strip_prefix("HKCU\\").unwrap_or(key_path),
207 )
208 } else if key_path.starts_with("HKLM") {
209 (
210 RegKey::predef(HKEY_LOCAL_MACHINE),
211 key_path.strip_prefix("HKLM\\").unwrap_or(key_path),
212 )
213 } else {
214 println!(
215 " {} Unknown registry hive: {}",
216 "[\u{2717}]".red(),
217 key_path
218 );
219 return;
220 };
221
222 match hive.open_subkey_with_flags(subkey, winreg::enums::KEY_WRITE) {
223 Ok(key) => match key.delete_value(value_name) {
224 Ok(_) => {
225 println!(
226 " {} Removed startup entry: {} from {}",
227 "[\u{2713}]".green().bold(),
228 value_name,
229 key_path
230 );
231 *count += 1;
232 }
233 Err(e) => {
234 println!(
235 " {} Failed to remove '{}': {}",
236 "[\u{2717}]".red(),
237 value_name,
238 e
239 );
240 }
241 },
242 Err(e) => {
243 println!(
244 " {} Cannot open registry key '{}': {} (may need admin)",
245 "[\u{2717}]".red(),
246 key_path,
247 e
248 );
249 }
250 }
251}
252
253/// Restore a quarantined file by ID
254pub fn restore_file(id: usize) -> Result<(), String> {
255 let mut manifest = QuarantineManifest::load();
256
257 let entry_idx = manifest
258 .entries
259 .iter()
260 .position(|e| e.id == id)
261 .ok_or_else(|| format!("No quarantined item with ID {}", id))?;
262
263 let entry = &manifest.entries[entry_idx];
264 let source = PathBuf::from(&entry.quarantine_path);
265 let dest = PathBuf::from(&entry.original_path);
266
267 if !source.exists() {
268 return Err(format!(
269 "Quarantine file not found: {}",
270 entry.quarantine_path
271 ));
272 }
273
274 if let Some(parent) = dest.parent() {
275 fs::create_dir_all(parent)
276 .map_err(|e| format!("Failed to create destination directory: {}", e))?;
277 }
278
279 fs::rename(&source, &dest)
280 .or_else(|_| {
281 fs::copy(&source, &dest)?;
282 fs::remove_file(&source)
283 })
284 .map_err(|e| format!("Failed to restore file: {}", e))?;
285
286 println!(
287 " {} Restored: {} -> {}",
288 "[\u{2713}]".green().bold(),
289 entry.quarantine_path,
290 entry.original_path
291 );
292
293 manifest.entries.remove(entry_idx);
294 manifest.save()?;
295
296 Ok(())
297}
298
299/// List all quarantined items
300pub fn list_quarantine() {
301 let manifest = QuarantineManifest::load();
302
303 if manifest.entries.is_empty() {
304 println!(
305 " {} No items in quarantine.",
306 "[i]".blue()
307 );
308 return;
309 }
310
311 println!(
312 "\n {} {} items in quarantine:\n",
313 "[i]".blue().bold(),
314 manifest.entries.len()
315 );
316
317 for entry in &manifest.entries {
318 println!(
319 " ID: {} | {} | {}",
320 entry.id.to_string().yellow().bold(),
321 entry.threat_name.white(),
322 entry.quarantined_at.dimmed()
323 );
324 println!(
325 " Original: {}",
326 entry.original_path.dimmed()
327 );
328 }
329
330 println!(
331 "\n To restore: {}",
332 "down --restore <ID>".yellow()
333 );
334}
335
336fn get_quarantine_dir() -> PathBuf {
337 if cfg!(windows) {
338 dirs::data_local_dir()
339 .unwrap_or_else(|| PathBuf::from("."))
340 .join("DownScanner")
341 .join("quarantine")
342 } else {
343 dirs::home_dir()
344 .unwrap_or_else(|| PathBuf::from("."))
345 .join(".down-scanner")
346 .join("quarantine")
347 }
348}
Addeddown-scanner/src/report.rs+182−0View fileUnifiedSplit
1use crate::threat::{Severity, Threat};
2use chrono::Local;
3use colored::*;
4use std::fs;
5use std::io::Write;
6use std::path::PathBuf;
7
8pub fn print_banner() {
9 println!(
10 "{}",
11 r#"
12 ____ _____ ___ _
13 | _ \ / _ \ \ / / \ | |
14 | | | | | | \ \ /\ / /| \| |
15 | |_| | |_| |\ V V / | |\ |
16 |____/ \___/ \_/\_/ |_| \_|
17 Windows Security Scanner v0.1.0
18"#
19 .cyan()
20 .bold()
21 );
22}
23
24pub fn print_scan_start(scan_type: &str) {
25 println!(
26 "{} {} {}",
27 "[*]".cyan().bold(),
28 "Starting".white(),
29 scan_type.yellow().bold()
30 );
31 println!("{}", "\u{2500}".repeat(60).dimmed());
32}
33
34pub fn print_module_start(module: &str) {
35 println!(
36 "\n{} {}",
37 "[>]".blue().bold(),
38 module.white().bold()
39 );
40}
41
42pub fn print_module_clean(module: &str) {
43 println!(
44 " {} {} \u{2014} {}",
45 "[\u{2713}]".green().bold(),
46 module.white(),
47 "Clean".green()
48 );
49}
50
51pub fn print_threat(threat: &Threat) {
52 let severity_colored = match threat.severity {
53 Severity::Critical => threat.severity.to_string().red().bold(),
54 Severity::High => threat.severity.to_string().red(),
55 Severity::Medium => threat.severity.to_string().yellow(),
56 Severity::Low => threat.severity.to_string().white(),
57 };
58
59 println!(
60 " {} [{}] {} \u{2014} {}",
61 "[!]".red().bold(),
62 severity_colored,
63 threat.name.white().bold(),
64 threat.category.to_string().dimmed()
65 );
66 println!(" Location: {}", threat.location.dimmed());
67 println!(" Detail: {}", threat.description);
68 println!(" Action: {}", threat.action.to_string().yellow());
69}
70
71pub fn print_summary(threats: &[Threat]) {
72 println!("\n{}", "\u{2550}".repeat(60).dimmed());
73 println!("{}", " SCAN SUMMARY".white().bold());
74 println!("{}", "\u{2550}".repeat(60).dimmed());
75
76 let critical = threats.iter().filter(|t| t.severity == Severity::Critical).count();
77 let high = threats.iter().filter(|t| t.severity == Severity::High).count();
78 let medium = threats.iter().filter(|t| t.severity == Severity::Medium).count();
79 let low = threats.iter().filter(|t| t.severity == Severity::Low).count();
80 let total = threats.len();
81
82 if total == 0 {
83 println!(
84 "\n {} {}",
85 "\u{2713}".green().bold(),
86 "No threats detected. Your system looks clean.".green().bold()
87 );
88 } else {
89 println!();
90 if critical > 0 {
91 println!(
92 " {} Critical: {}",
93 "\u{25cf}".red().bold(),
94 critical.to_string().red().bold()
95 );
96 }
97 if high > 0 {
98 println!(
99 " {} High: {}",
100 "\u{25cf}".red(),
101 high.to_string().red()
102 );
103 }
104 if medium > 0 {
105 println!(
106 " {} Medium: {}",
107 "\u{25cf}".yellow(),
108 medium.to_string().yellow()
109 );
110 }
111 if low > 0 {
112 println!(
113 " {} Low: {}",
114 "\u{25cf}".white(),
115 low.to_string().white()
116 );
117 }
118 println!(
119 "\n Total threats found: {}",
120 total.to_string().red().bold()
121 );
122 println!(
123 "\n {} Run {} to remove detected threats.",
124 "\u{2192}".cyan(),
125 "down --quarantine".yellow().bold()
126 );
127 }
128 println!("{}\n", "\u{2550}".repeat(60).dimmed());
129}
130
131/// Write scan results to a log file
132pub fn write_log(threats: &[Threat]) -> Result<PathBuf, String> {
133 let log_dir = get_log_dir();
134 fs::create_dir_all(&log_dir).map_err(|e| format!("Failed to create log directory: {}", e))?;
135
136 let timestamp = Local::now().format("%Y-%m-%d_%H-%M-%S");
137 let log_path = log_dir.join(format!("scan-{}.log", timestamp));
138
139 let mut file =
140 fs::File::create(&log_path).map_err(|e| format!("Failed to create log file: {}", e))?;
141
142 writeln!(file, "DOWN Security Scanner \u{2014} Scan Report").ok();
143 writeln!(file, "Date: {}", Local::now().format("%Y-%m-%d %H:%M:%S")).ok();
144 writeln!(file, "Threats found: {}", threats.len()).ok();
145 writeln!(file, "{}", "=".repeat(60)).ok();
146
147 for (i, threat) in threats.iter().enumerate() {
148 writeln!(file, "\n--- Threat #{} ---", i + 1).ok();
149 writeln!(file, "Name: {}", threat.name).ok();
150 writeln!(file, "Severity: {}", threat.severity).ok();
151 writeln!(file, "Category: {}", threat.category).ok();
152 writeln!(file, "Location: {}", threat.location).ok();
153 writeln!(file, "Detail: {}", threat.description).ok();
154 writeln!(file, "Action: {}", threat.action).ok();
155 }
156
157 writeln!(file, "\n{}", "=".repeat(60)).ok();
158 writeln!(file, "End of report.").ok();
159
160 println!(
161 " {} Log saved to: {}",
162 "[i]".blue(),
163 log_path.display().to_string().dimmed()
164 );
165
166 Ok(log_path)
167}
168
169fn get_log_dir() -> PathBuf {
170 if cfg!(windows) {
171 dirs::data_local_dir()
172 .unwrap_or_else(|| PathBuf::from("."))
173 .join("DownScanner")
174 .join("logs")
175 } else {
176 // For development/testing on Linux/Mac
177 dirs::home_dir()
178 .unwrap_or_else(|| PathBuf::from("."))
179 .join(".down-scanner")
180 .join("logs")
181 }
182}
Addeddown-scanner/src/scanner/browser.rs+142−0View fileUnifiedSplit
1use crate::signatures::extension_ids::{KNOWN_BAD_EXTENSIONS, SUSPICIOUS_PERMISSIONS, SUSPICIOUS_PERMISSION_THRESHOLD};
2use crate::threat::{Severity, Threat, ThreatAction, ThreatCategory};
3use std::fs;
4use std::path::{Path, PathBuf};
5
6pub fn scan() -> Vec<Threat> {
7 let mut threats = Vec::new();
8 for profile_dir in get_chrome_extension_dirs() { scan_chromium_extensions(&profile_dir, "Chrome", &mut threats); }
9 for profile_dir in get_edge_extension_dirs() { scan_chromium_extensions(&profile_dir, "Edge", &mut threats); }
10 for profile_dir in get_firefox_extension_dirs() { scan_firefox_extensions(&profile_dir, &mut threats); }
11 threats
12}
13
14fn scan_chromium_extensions(extensions_dir: &Path, browser: &str, threats: &mut Vec<Threat>) {
15 if !extensions_dir.exists() { return; }
16 let entries = match fs::read_dir(extensions_dir) { Ok(e) => e, Err(_) => return };
17 for entry in entries.flatten() {
18 if !entry.path().is_dir() { continue; }
19 let ext_id = entry.file_name().to_string_lossy().to_string();
20 for (bad_id, name, reason) in KNOWN_BAD_EXTENSIONS {
21 if ext_id == *bad_id {
22 threats.push(Threat {
23 name: format!("{}: {}", browser, name),
24 severity: Severity::Critical,
25 category: ThreatCategory::BrowserHijacker,
26 location: entry.path().to_string_lossy().to_string(),
27 description: format!("Known malicious {} extension '{}' (ID: {}). Reason: {}", browser, name, ext_id, reason),
28 action: ThreatAction::QuarantineFile(entry.path().to_string_lossy().to_string()),
29 });
30 break;
31 }
32 }
33 if let Some(manifest) = find_manifest(&entry.path()) {
34 check_extension_permissions(&manifest, &ext_id, browser, &entry.path(), threats);
35 }
36 }
37}
38
39fn find_manifest(ext_dir: &Path) -> Option<serde_json::Value> {
40 let entries: Vec<_> = fs::read_dir(ext_dir).ok()?.flatten().collect();
41 let direct = ext_dir.join("manifest.json");
42 if direct.exists() {
43 let content = fs::read_to_string(&direct).ok()?;
44 return serde_json::from_str(&content).ok();
45 }
46 for entry in entries.iter().rev() {
47 let manifest_path = entry.path().join("manifest.json");
48 if manifest_path.exists() {
49 let content = fs::read_to_string(&manifest_path).ok()?;
50 return serde_json::from_str(&content).ok();
51 }
52 }
53 None
54}
55
56fn check_extension_permissions(manifest: &serde_json::Value, ext_id: &str, browser: &str, ext_path: &Path, threats: &mut Vec<Threat>) {
57 let ext_name = manifest.get("name").and_then(|n| n.as_str()).unwrap_or("Unknown Extension");
58 let mut all_permissions = Vec::new();
59 for key in &["permissions", "optional_permissions", "host_permissions"] {
60 if let Some(perms) = manifest.get(*key).and_then(|p| p.as_array()) {
61 for p in perms { if let Some(s) = p.as_str() { all_permissions.push(s.to_string()); } }
62 }
63 }
64 let suspicious_count = all_permissions.iter().filter(|p| SUSPICIOUS_PERMISSIONS.iter().any(|sp| p.to_lowercase().contains(&sp.to_lowercase()))).count();
65 if suspicious_count >= SUSPICIOUS_PERMISSION_THRESHOLD {
66 let already_flagged = threats.iter().any(|t| t.location == ext_path.to_string_lossy());
67 if !already_flagged {
68 let perm_list: Vec<&str> = all_permissions.iter().filter(|p| SUSPICIOUS_PERMISSIONS.iter().any(|sp| p.to_lowercase().contains(&sp.to_lowercase()))).map(|s| s.as_str()).collect();
69 threats.push(Threat {
70 name: format!("{}: Excessive permissions \u{2014} {}", browser, ext_name),
71 severity: Severity::High,
72 category: ThreatCategory::PotentiallyUnwanted,
73 location: ext_path.to_string_lossy().to_string(),
74 description: format!("Extension '{}' (ID: {}) requests {} suspicious permissions: {}", ext_name, ext_id, suspicious_count, perm_list.join(", ")),
75 action: ThreatAction::ManualReview,
76 });
77 }
78 }
79}
80
81fn scan_firefox_extensions(profile_dir: &Path, threats: &mut Vec<Threat>) {
82 let extensions_dir = profile_dir.join("extensions");
83 if !extensions_dir.exists() { return; }
84 let entries = match fs::read_dir(&extensions_dir) { Ok(e) => e, Err(_) => return };
85 for entry in entries.flatten() {
86 let file_name = entry.file_name().to_string_lossy().to_string();
87 for (bad_id, name, reason) in KNOWN_BAD_EXTENSIONS {
88 if file_name.contains(bad_id) {
89 threats.push(Threat {
90 name: format!("Firefox: {}", name),
91 severity: Severity::Critical,
92 category: ThreatCategory::BrowserHijacker,
93 location: entry.path().to_string_lossy().to_string(),
94 description: format!("Known malicious Firefox extension '{}'. Reason: {}", name, reason),
95 action: ThreatAction::QuarantineFile(entry.path().to_string_lossy().to_string()),
96 });
97 break;
98 }
99 }
100 }
101}
102
103fn get_chrome_extension_dirs() -> Vec<PathBuf> {
104 let mut dirs = Vec::new();
105 if let Some(local) = dirs::data_local_dir() {
106 let base = local.join("Google").join("Chrome").join("User Data");
107 dirs.push(base.join("Default").join("Extensions"));
108 if let Ok(entries) = fs::read_dir(&base) {
109 for entry in entries.flatten() {
110 let name = entry.file_name().to_string_lossy().to_string();
111 if name.starts_with("Profile ") { dirs.push(entry.path().join("Extensions")); }
112 }
113 }
114 }
115 dirs
116}
117
118fn get_edge_extension_dirs() -> Vec<PathBuf> {
119 let mut dirs = Vec::new();
120 if let Some(local) = dirs::data_local_dir() {
121 let base = local.join("Microsoft").join("Edge").join("User Data");
122 dirs.push(base.join("Default").join("Extensions"));
123 if let Ok(entries) = fs::read_dir(&base) {
124 for entry in entries.flatten() {
125 let name = entry.file_name().to_string_lossy().to_string();
126 if name.starts_with("Profile ") { dirs.push(entry.path().join("Extensions")); }
127 }
128 }
129 }
130 dirs
131}
132
133fn get_firefox_extension_dirs() -> Vec<PathBuf> {
134 let mut dirs = Vec::new();
135 if let Some(roaming) = dirs::config_dir() {
136 let profiles_dir = roaming.join("Mozilla").join("Firefox").join("Profiles");
137 if let Ok(entries) = fs::read_dir(&profiles_dir) {
138 for entry in entries.flatten() { if entry.path().is_dir() { dirs.push(entry.path()); } }
139 }
140 }
141 dirs
142}
Addeddown-scanner/src/scanner/files.rs+150−0View fileUnifiedSplit
1use crate::signatures::hashes::KNOWN_MALWARE_HASHES;
2use crate::threat::{Severity, Threat, ThreatAction, ThreatCategory};
3use sha2::{Digest, Sha256};
4use std::fs;
5use std::io::Read;
6use std::path::{Path, PathBuf};
7use walkdir::WalkDir;
8
9const MAX_HASH_SIZE: u64 = 50 * 1024 * 1024;
10const MAX_SCAN_DEPTH: usize = 5;
11
12const DOUBLE_EXTENSIONS: &[(&str, &str)] = &[
13 (".pdf.exe", "PDF disguised as executable"),
14 (".doc.exe", "Word doc disguised as executable"),
15 (".docx.exe", "Word doc disguised as executable"),
16 (".xls.exe", "Excel disguised as executable"),
17 (".xlsx.exe", "Excel disguised as executable"),
18 (".jpg.exe", "Image disguised as executable"),
19 (".jpeg.exe", "Image disguised as executable"),
20 (".png.exe", "Image disguised as executable"),
21 (".gif.exe", "Image disguised as executable"),
22 (".mp3.exe", "Audio disguised as executable"),
23 (".mp4.exe", "Video disguised as executable"),
24 (".txt.exe", "Text file disguised as executable"),
25 (".pdf.scr", "PDF disguised as screensaver"),
26 (".doc.scr", "Word doc disguised as screensaver"),
27 (".jpg.scr", "Image disguised as screensaver"),
28 (".pdf.bat", "PDF disguised as batch file"),
29 (".doc.bat", "Word doc disguised as batch file"),
30 (".pdf.cmd", "PDF disguised as command file"),
31 (".pdf.vbs", "PDF disguised as VBScript"),
32 (".doc.vbs", "Word doc disguised as VBScript"),
33 (".pdf.js", "PDF disguised as JavaScript"),
34 (".pdf.ps1", "PDF disguised as PowerShell"),
35];
36
37const EXECUTABLE_EXTENSIONS: &[&str] = &[
38 ".exe", ".scr", ".bat", ".cmd", ".vbs", ".vbe", ".js", ".jse",
39 ".wsf", ".wsh", ".ps1", ".msi", ".dll", ".com", ".pif",
40];
41
42pub fn scan() -> Vec<Threat> {
43 let mut threats = Vec::new();
44 let scan_dirs = get_scan_directories();
45 for dir in &scan_dirs {
46 if !dir.exists() { continue; }
47 scan_directory(dir, &mut threats);
48 }
49 threats
50}
51
52fn scan_directory(dir: &Path, threats: &mut Vec<Threat>) {
53 let walker = WalkDir::new(dir).max_depth(MAX_SCAN_DEPTH).follow_links(false).into_iter().filter_map(|e| e.ok());
54 for entry in walker {
55 let path = entry.path();
56 if path.is_dir() { continue; }
57 let file_name = path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
58 let file_name_lower = file_name.to_lowercase();
59 let path_str = path.to_string_lossy().to_string();
60
61 for (double_ext, description) in DOUBLE_EXTENSIONS {
62 if file_name_lower.ends_with(double_ext) {
63 threats.push(Threat {
64 name: format!("Double extension: {}", file_name),
65 severity: Severity::Critical,
66 category: ThreatCategory::Malware,
67 location: path_str.clone(),
68 description: format!("File '{}' has a deceptive double extension \u{2014} {}. This is a common malware technique.", file_name, description),
69 action: ThreatAction::QuarantineFile(path_str.clone()),
70 });
71 break;
72 }
73 }
74
75 let is_executable = EXECUTABLE_EXTENSIONS.iter().any(|ext| file_name_lower.ends_with(ext));
76 let in_temp = path_str.to_lowercase().contains("\\temp\\") || path_str.to_lowercase().contains("/tmp/");
77
78 if is_executable && in_temp {
79 if let Some(hash) = hash_file(path) {
80 for (known_hash, malware_name) in KNOWN_MALWARE_HASHES {
81 if hash == *known_hash {
82 threats.push(Threat {
83 name: format!("Known malware: {}", malware_name),
84 severity: Severity::Critical,
85 category: ThreatCategory::Malware,
86 location: path_str.clone(),
87 description: format!("File '{}' matches known malware hash for '{}'. SHA256: {}", file_name, malware_name, hash),
88 action: ThreatAction::QuarantineFile(path_str.clone()),
89 });
90 break;
91 }
92 }
93 }
94 }
95
96 if is_executable && !in_standard_program_location(&path_str) {
97 if let Ok(metadata) = fs::metadata(path) {
98 if let Ok(modified) = metadata.modified() {
99 if let Ok(age) = std::time::SystemTime::now().duration_since(modified) {
100 if age.as_secs() < 86400 {
101 let already_flagged = threats.iter().any(|t| t.location == path_str);
102 if !already_flagged {
103 threats.push(Threat {
104 name: format!("Recently modified executable: {}", file_name),
105 severity: Severity::Medium,
106 category: ThreatCategory::SuspiciousFile,
107 location: path_str.clone(),
108 description: format!("Executable '{}' was modified in the last 24 hours and is in a non-standard location.", file_name),
109 action: ThreatAction::ManualReview,
110 });
111 }
112 }
113 }
114 }
115 }
116 }
117 }
118}
119
120fn hash_file(path: &Path) -> Option<String> {
121 let metadata = fs::metadata(path).ok()?;
122 if metadata.len() > MAX_HASH_SIZE { return None; }
123 let mut file = fs::File::open(path).ok()?;
124 let mut hasher = Sha256::new();
125 let mut buffer = [0u8; 8192];
126 loop {
127 let bytes_read = file.read(&mut buffer).ok()?;
128 if bytes_read == 0 { break; }
129 hasher.update(&buffer[..bytes_read]);
130 }
131 Some(format!("{:x}", hasher.finalize()))
132}
133
134fn in_standard_program_location(path: &str) -> bool {
135 let p = path.to_lowercase();
136 p.contains("\\program files\\") || p.contains("\\program files (x86)\\") || p.contains("\\windows\\") || p.contains("\\system32\\") || p.contains("\\syswow64\\") || p.contains("/usr/") || p.contains("/bin/") || p.contains("/sbin/")
137}
138
139fn get_scan_directories() -> Vec<PathBuf> {
140 let mut dirs = Vec::new();
141 if let Some(download) = dirs::download_dir() { dirs.push(download); }
142 if let Some(desktop) = dirs::desktop_dir() { dirs.push(desktop); }
143 dirs.push(std::env::temp_dir());
144 if cfg!(windows) {
145 if let Some(local) = dirs::data_local_dir() { dirs.push(local.join("Temp")); }
146 if let Some(roaming) = dirs::config_dir() { dirs.push(roaming); }
147 dirs.push(PathBuf::from(r"C:\Users\Public"));
148 }
149 dirs
150}
Addeddown-scanner/src/scanner/mod.rs+6−0View fileUnifiedSplit
1pub mod browser;
2pub mod files;
3pub mod network;
4pub mod processes;
5pub mod scareware;
6pub mod startup;
Addeddown-scanner/src/scanner/network.rs+145−0View fileUnifiedSplit
1use crate::signatures::ip_blocklist::{KNOWN_BAD_DOMAINS, KNOWN_BAD_DNS, KNOWN_BAD_IP_PREFIXES, LEGITIMATE_HOSTS_ENTRIES};
2use crate::threat::{Severity, Threat, ThreatAction, ThreatCategory};
3use std::fs;
4use std::path::Path;
5
6pub fn scan() -> Vec<Threat> {
7 let mut threats = Vec::new();
8 check_hosts_file(&mut threats);
9 #[cfg(windows)] check_network_connections(&mut threats);
10 #[cfg(windows)] check_dns_settings(&mut threats);
11 #[cfg(not(windows))] check_resolv_conf(&mut threats);
12 threats
13}
14
15fn check_hosts_file(threats: &mut Vec<Threat>) {
16 let hosts_path = if cfg!(windows) { Path::new(r"C:\Windows\System32\drivers\etc\hosts") } else { Path::new("/etc/hosts") };
17 let content = match fs::read_to_string(hosts_path) { Ok(c) => c, Err(_) => return };
18 let mut suspicious_entries = Vec::new();
19 for line in content.lines() {
20 let trimmed = line.trim();
21 if trimmed.is_empty() || trimmed.starts_with('#') { continue; }
22 let parts: Vec<&str> = trimmed.split_whitespace().collect();
23 if parts.len() < 2 { continue; }
24 let ip = parts[0];
25 let hostnames: Vec<&str> = parts[1..].to_vec();
26 for hostname in &hostnames {
27 let is_legitimate = LEGITIMATE_HOSTS_ENTRIES.iter().any(|legit| hostname.eq_ignore_ascii_case(legit));
28 if !is_legitimate {
29 let is_redirect = ip != "127.0.0.1" && ip != "::1" && ip != "0.0.0.0";
30 let is_blocking = ip == "127.0.0.1" || ip == "0.0.0.0";
31 if is_redirect {
32 suspicious_entries.push(format!("{} -> {} (REDIRECT)", hostname, ip));
33 } else if is_blocking {
34 let important_domains = ["windowsupdate", "microsoft.com", "google.com", "chrome.google.com", "update.googleapis.com"];
35 if important_domains.iter().any(|d| hostname.to_lowercase().contains(d)) {
36 suspicious_entries.push(format!("{} BLOCKED by hosts file (could prevent updates)", hostname));
37 }
38 }
39 }
40 }
41 for (bad_prefix, description) in KNOWN_BAD_IP_PREFIXES {
42 if ip.starts_with(bad_prefix) {
43 threats.push(Threat {
44 name: "Hosts file points to malicious IP".to_string(),
45 severity: Severity::Critical, category: ThreatCategory::HostsTampering,
46 location: hosts_path.to_string_lossy().to_string(),
47 description: format!("Hosts entry '{}' redirects to suspicious IP {} \u{2014} {}", hostnames.join(", "), ip, description),
48 action: ThreatAction::ManualReview,
49 });
50 }
51 }
52 for (bad_domain, description) in KNOWN_BAD_DOMAINS {
53 for hostname in &hostnames {
54 if hostname.to_lowercase().contains(bad_domain) {
55 threats.push(Threat {
56 name: format!("Known bad domain in hosts: {}", hostname),
57 severity: Severity::High, category: ThreatCategory::HostsTampering,
58 location: hosts_path.to_string_lossy().to_string(),
59 description: format!("Hosts file references known malicious domain pattern '{}' \u{2014} {}", bad_domain, description),
60 action: ThreatAction::ManualReview,
61 });
62 }
63 }
64 }
65 }
66 if !suspicious_entries.is_empty() {
67 threats.push(Threat {
68 name: "Hosts file modifications detected".to_string(),
69 severity: Severity::High, category: ThreatCategory::HostsTampering,
70 location: hosts_path.to_string_lossy().to_string(),
71 description: format!("Found {} suspicious entries in hosts file:\n {}", suspicious_entries.len(), suspicious_entries.join("\n ")),
72 action: ThreatAction::ManualReview,
73 });
74 }
75}
76
77#[cfg(windows)]
78fn check_network_connections(threats: &mut Vec<Threat>) {
79 let output = match std::process::Command::new("netstat").args(["-an"]).output() { Ok(o) => o, Err(_) => return };
80 let stdout = String::from_utf8_lossy(&output.stdout);
81 for line in stdout.lines() {
82 let parts: Vec<&str> = line.split_whitespace().collect();
83 if parts.len() < 3 { continue; }
84 let remote = parts.get(2).unwrap_or(&"");
85 for (bad_prefix, description) in KNOWN_BAD_IP_PREFIXES {
86 if remote.starts_with(bad_prefix) {
87 threats.push(Threat {
88 name: format!("Connection to suspicious IP: {}", remote),
89 severity: Severity::Critical, category: ThreatCategory::SuspiciousNetwork,
90 location: format!("Active connection: {} -> {}", parts.get(1).unwrap_or(&"?"), remote),
91 description: format!("Active network connection to known suspicious IP range \u{2014} {}", description),
92 action: ThreatAction::ManualReview,
93 });
94 }
95 }
96 }
97}
98
99#[cfg(windows)]
100fn check_dns_settings(threats: &mut Vec<Threat>) {
101 let output = match std::process::Command::new("ipconfig").args(["/all"]).output() { Ok(o) => o, Err(_) => return };
102 let stdout = String::from_utf8_lossy(&output.stdout);
103 for line in stdout.lines() {
104 let trimmed = line.trim();
105 if trimmed.contains("DNS Servers") || trimmed.contains("DNS-Server") {
106 if let Some(ip_part) = trimmed.split(':').nth(1) {
107 let ip = ip_part.trim();
108 for (bad_dns, description) in KNOWN_BAD_DNS {
109 if ip.starts_with(bad_dns) {
110 threats.push(Threat {
111 name: format!("Malicious DNS server: {}", ip),
112 severity: Severity::Critical, category: ThreatCategory::DnsTampering,
113 location: "Network adapter DNS settings".to_string(),
114 description: format!("DNS server {} is known malicious \u{2014} {}. Your DNS queries may be intercepted.", ip, description),
115 action: ThreatAction::ManualReview,
116 });
117 }
118 }
119 }
120 }
121 }
122}
123
124#[cfg(not(windows))]
125fn check_resolv_conf(threats: &mut Vec<Threat>) {
126 let content = match fs::read_to_string("/etc/resolv.conf") { Ok(c) => c, Err(_) => return };
127 for line in content.lines() {
128 let trimmed = line.trim();
129 if trimmed.starts_with("nameserver") {
130 if let Some(ip) = trimmed.split_whitespace().nth(1) {
131 for (bad_dns, description) in KNOWN_BAD_DNS {
132 if ip.starts_with(bad_dns) {
133 threats.push(Threat {
134 name: format!("Malicious DNS server: {}", ip),
135 severity: Severity::Critical, category: ThreatCategory::DnsTampering,
136 location: "/etc/resolv.conf".to_string(),
137 description: format!("DNS server {} is known malicious \u{2014} {}", ip, description),
138 action: ThreatAction::ManualReview,
139 });
140 }
141 }
142 }
143 }
144 }
145}
Addeddown-scanner/src/scanner/processes.rs+88−0View fileUnifiedSplit
1use crate::signatures::process_names::{
2 KNOWN_BAD_PROCESSES, KNOWN_SAFE_PROCESSES, SUSPICIOUS_PATH_FRAGMENTS,
3};
4use crate::threat::{Severity, Threat, ThreatAction, ThreatCategory};
5use sysinfo::System;
6
7const CRYPTOMINER_CPU_THRESHOLD: f32 = 80.0;
8
9pub fn scan(system: &System) -> Vec<Threat> {
10 let mut threats = Vec::new();
11
12 for (pid, process) in system.processes() {
13 let name = process.name().to_string_lossy().to_lowercase();
14 let exe_path = process
15 .exe()
16 .map(|p| p.to_string_lossy().to_lowercase())
17 .unwrap_or_default();
18 let pid_u32 = pid.as_u32();
19
20 for bad_name in KNOWN_BAD_PROCESSES {
21 if name.contains(bad_name) {
22 threats.push(Threat {
23 name: format!("Known bad process: {}", process.name().to_string_lossy()),
24 severity: Severity::Critical,
25 category: ThreatCategory::Malware,
26 location: exe_path.clone(),
27 description: format!(
28 "Process '{}' (PID: {}) matches known malware/scareware signature '{}'",
29 process.name().to_string_lossy(),
30 pid_u32,
31 bad_name
32 ),
33 action: ThreatAction::KillProcess(pid_u32),
34 });
35 break;
36 }
37 }
38
39 let name_lower = name.clone();
40 if KNOWN_SAFE_PROCESSES.iter().any(|s| name_lower.contains(s)) {
41 continue;
42 }
43
44 if !exe_path.is_empty() {
45 for fragment in SUSPICIOUS_PATH_FRAGMENTS {
46 if exe_path.contains(fragment) {
47 threats.push(Threat {
48 name: format!(
49 "Suspicious location: {}",
50 process.name().to_string_lossy()
51 ),
52 severity: Severity::High,
53 category: ThreatCategory::SuspiciousProcess,
54 location: exe_path.clone(),
55 description: format!(
56 "Process '{}' (PID: {}) is running from suspicious path containing '{}'",
57 process.name().to_string_lossy(),
58 pid_u32,
59 fragment
60 ),
61 action: ThreatAction::KillProcess(pid_u32),
62 });
63 break;
64 }
65 }
66 }
67
68 let cpu = process.cpu_usage();
69 if cpu > CRYPTOMINER_CPU_THRESHOLD {
70 threats.push(Threat {
71 name: format!("High CPU usage: {}", process.name().to_string_lossy()),
72 severity: Severity::Medium,
73 category: ThreatCategory::Cryptominer,
74 location: exe_path.clone(),
75 description: format!(
76 "Process '{}' (PID: {}) using {:.1}% CPU \u{2014} possible cryptominer. \
77 Check if this is a legitimate program (e.g., video encoding, game).",
78 process.name().to_string_lossy(),
79 pid_u32,
80 cpu
81 ),
82 action: ThreatAction::ManualReview,
83 });
84 }
85 }
86
87 threats
88}
Addeddown-scanner/src/scanner/scareware.rs+157−0View fileUnifiedSplit
1use crate::signatures::process_names::KNOWN_BAD_PROCESSES;
2use crate::threat::{Severity, Threat, ThreatAction, ThreatCategory};
3use std::fs;
4use std::path::PathBuf;
5
6static SCAREWARE_DISPLAY_NAMES: &[&str] = &[
7 "PC Protect", "PCProtect", "Total AV", "TotalAV", "Scanguard", "ScanGuard",
8 "Segurazo", "SegurazoAV", "RAV Antivirus", "RAV Endpoint Protection",
9 "ByteFence", "ByteFence Anti-Malware", "SpyHunter", "SpyHunter 5",
10 "WinAntiVirus", "WinFixer", "ErrorSafe", "DriveCleaner", "System Doctor",
11 "MyPCBackup", "PC Keeper", "MacKeeper",
12 "Norton Security Scan Free", "McAfee Total Security Free",
13 "Kaspersky Free Scan", "Windows Defender Alert",
14 "Restoro", "Fortect", "Outbyte PC Repair", "Outbyte Driver Updater",
15 "Reimage Repair", "Reimage PC Repair", "Smart PC Fixer", "RegClean Pro",
16 "Registry Mechanic", "Registry Booster", "WinZip Driver Updater",
17 "Driver Updater", "DriverUpdate", "Driver Easy", "SlimCleaner",
18 "SlimCleaner Plus", "MyCleanPC", "One Click PC Care", "Speed My PC",
19 "Advanced SystemCare", "Systweak", "iolo System Mechanic",
20 "PC Optimizer Pro", "Xtreme Speed Booster", "Super PC Cleaner",
21 "Max PC Tuner", "Win Tonic", "OneSafe PC Cleaner", "Qihoo 360 Total Security",
22 "Search Protect", "Conduit Search", "Ask Toolbar", "Babylon Toolbar",
23 "Delta Toolbar", "MindSpark", "MyWebSearch", "SweetIM", "Snap.do",
24 "Trovi", "SafeFinder",
25];
26
27pub fn scan() -> Vec<Threat> {
28 let mut threats = Vec::new();
29 scan_program_dirs(&mut threats);
30 #[cfg(windows)] scan_installed_programs_registry(&mut threats);
31 #[cfg(windows)] scan_scheduled_tasks(&mut threats);
32 threats
33}
34
35fn scan_program_dirs(threats: &mut Vec<Threat>) {
36 let program_dirs = get_program_directories();
37 for dir in &program_dirs {
38 if !dir.exists() { continue; }
39 let entries = match fs::read_dir(dir) { Ok(e) => e, Err(_) => continue };
40 for entry in entries.flatten() {
41 if !entry.path().is_dir() { continue; }
42 let folder_name = entry.file_name().to_string_lossy().to_string();
43 let folder_lower = folder_name.to_lowercase();
44 for scareware_name in SCAREWARE_DISPLAY_NAMES {
45 if folder_lower.contains(&scareware_name.to_lowercase()) {
46 threats.push(Threat {
47 name: format!("Scareware installed: {}", folder_name),
48 severity: Severity::High, category: ThreatCategory::Scareware,
49 location: entry.path().to_string_lossy().to_string(),
50 description: format!("Program folder '{}' matches known scareware '{}'. These programs often show fake scan results to trick you into paying.", folder_name, scareware_name),
51 action: ThreatAction::QuarantineFile(entry.path().to_string_lossy().to_string()),
52 });
53 break;
54 }
55 }
56 for bad_name in KNOWN_BAD_PROCESSES {
57 if folder_lower.contains(bad_name) {
58 let already_flagged = threats.iter().any(|t| t.location == entry.path().to_string_lossy());
59 if !already_flagged {
60 threats.push(Threat {
61 name: format!("Suspicious program: {}", folder_name),
62 severity: Severity::High, category: ThreatCategory::PotentiallyUnwanted,
63 location: entry.path().to_string_lossy().to_string(),
64 description: format!("Program folder '{}' matches known threat signature '{}'", folder_name, bad_name),
65 action: ThreatAction::ManualReview,
66 });
67 break;
68 }
69 }
70 }
71 }
72 }
73}
74
75#[cfg(windows)]
76fn scan_installed_programs_registry(threats: &mut Vec<Threat>) {
77 use winreg::enums::*;
78 use winreg::RegKey;
79 let uninstall_paths = [
80 (HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"),
81 (HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"),
82 (HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"),
83 ];
84 for (hive_key, path) in &uninstall_paths {
85 let hive = RegKey::predef(*hive_key);
86 let key = match hive.open_subkey(path) { Ok(k) => k, Err(_) => continue };
87 for subkey_name in key.enum_keys().flatten() {
88 let subkey = match key.open_subkey(&subkey_name) { Ok(k) => k, Err(_) => continue };
89 let display_name: String = subkey.get_value("DisplayName").unwrap_or_default();
90 if display_name.is_empty() { continue; }
91 let display_lower = display_name.to_lowercase();
92 for scareware_name in SCAREWARE_DISPLAY_NAMES {
93 if display_lower.contains(&scareware_name.to_lowercase()) {
94 let install_location: String = subkey.get_value("InstallLocation").unwrap_or_default();
95 let uninstall_string: String = subkey.get_value("UninstallString").unwrap_or_default();
96 threats.push(Threat {
97 name: format!("Installed scareware: {}", display_name),
98 severity: Severity::High, category: ThreatCategory::Scareware,
99 location: if install_location.is_empty() { format!("Registry: {}\\{}", path, subkey_name) } else { install_location },
100 description: format!("Installed program '{}' matches known scareware '{}'. Uninstall command: {}", display_name, scareware_name, if uninstall_string.is_empty() { "Not available".to_string() } else { uninstall_string }),
101 action: ThreatAction::ManualReview,
102 });
103 break;
104 }
105 }
106 }
107 }
108}
109
110#[cfg(windows)]
111fn scan_scheduled_tasks(threats: &mut Vec<Threat>) {
112 let output = match std::process::Command::new("schtasks").args(["/query", "/fo", "CSV", "/v"]).output() { Ok(o) => o, Err(_) => return };
113 let stdout = String::from_utf8_lossy(&output.stdout);
114 for line in stdout.lines().skip(1) {
115 let lower = line.to_lowercase();
116 for bad_name in KNOWN_BAD_PROCESSES {
117 if lower.contains(bad_name) {
118 threats.push(Threat {
119 name: format!("Suspicious scheduled task matching '{}'", bad_name),
120 severity: Severity::High, category: ThreatCategory::SuspiciousStartup,
121 location: "Windows Task Scheduler".to_string(),
122 description: format!("Scheduled task matches known threat '{}'. Task details: {}", bad_name, line.chars().take(200).collect::<String>()),
123 action: ThreatAction::ManualReview,
124 });
125 break;
126 }
127 }
128 for scareware_name in SCAREWARE_DISPLAY_NAMES {
129 if lower.contains(&scareware_name.to_lowercase()) {
130 let already = threats.iter().any(|t| t.location == "Windows Task Scheduler" && t.description.contains(scareware_name));
131 if !already {
132 threats.push(Threat {
133 name: format!("Scareware scheduled task: {}", scareware_name),
134 severity: Severity::High, category: ThreatCategory::Scareware,
135 location: "Windows Task Scheduler".to_string(),
136 description: format!("Scheduled task for known scareware '{}' found. This keeps the scareware running.", scareware_name),
137 action: ThreatAction::ManualReview,
138 });
139 }
140 break;
141 }
142 }
143 }
144}
145
146fn get_program_directories() -> Vec<PathBuf> {
147 let mut dirs = Vec::new();
148 if cfg!(windows) {
149 dirs.push(PathBuf::from(r"C:\Program Files"));
150 dirs.push(PathBuf::from(r"C:\Program Files (x86)"));
151 if let Some(local) = dirs::data_local_dir() { dirs.push(local.join("Programs")); }
152 } else {
153 dirs.push(PathBuf::from("/usr/local/bin"));
154 dirs.push(PathBuf::from("/opt"));
155 }
156 dirs
157}
Addeddown-scanner/src/scanner/startup.rs+153−0View fileUnifiedSplit
1use crate::signatures::process_names::KNOWN_BAD_PROCESSES;
2#[cfg(windows)]
3use crate::signatures::process_names::SUSPICIOUS_PATH_FRAGMENTS;
4use crate::threat::{Severity, Threat, ThreatAction, ThreatCategory};
5use std::path::Path;
6
7#[cfg(windows)]
8const REGISTRY_RUN_KEYS: &[(&str, &str)] = &[
9 ("HKCU", r"Software\Microsoft\Windows\CurrentVersion\Run"),
10 ("HKCU", r"Software\Microsoft\Windows\CurrentVersion\RunOnce"),
11 ("HKLM", r"Software\Microsoft\Windows\CurrentVersion\Run"),
12 ("HKLM", r"Software\Microsoft\Windows\CurrentVersion\RunOnce"),
13];
14
15pub fn scan() -> Vec<Threat> {
16 let mut threats = Vec::new();
17 #[cfg(windows)]
18 { scan_registry_keys(&mut threats); }
19 scan_startup_folders(&mut threats);
20 threats
21}
22
23#[cfg(windows)]
24fn scan_registry_keys(threats: &mut Vec<Threat>) {
25 use winreg::enums::*;
26 use winreg::RegKey;
27
28 for (hive_name, subkey_path) in REGISTRY_RUN_KEYS {
29 let hive = match *hive_name {
30 "HKCU" => RegKey::predef(HKEY_CURRENT_USER),
31 "HKLM" => RegKey::predef(HKEY_LOCAL_MACHINE),
32 _ => continue,
33 };
34 let key = match hive.open_subkey(subkey_path) {
35 Ok(k) => k,
36 Err(_) => continue,
37 };
38 for value_result in key.enum_values() {
39 let (value_name, value_data) = match value_result {
40 Ok(v) => v,
41 Err(_) => continue,
42 };
43 let value_str = format!("{:?}", value_data);
44 let value_lower = value_str.to_lowercase();
45 let full_key = format!("{}\\{}", hive_name, subkey_path);
46
47 for bad_name in KNOWN_BAD_PROCESSES {
48 if value_lower.contains(bad_name) || value_name.to_lowercase().contains(bad_name) {
49 threats.push(Threat {
50 name: format!("Malicious startup entry: {}", value_name),
51 severity: Severity::Critical,
52 category: ThreatCategory::SuspiciousStartup,
53 location: full_key.clone(),
54 description: format!("Registry startup entry '{}' matches known threat '{}'. Value: {}", value_name, bad_name, value_str),
55 action: ThreatAction::RemoveStartupEntry { key_path: full_key.clone(), value_name: value_name.clone() },
56 });
57 break;
58 }
59 }
60
61 for fragment in SUSPICIOUS_PATH_FRAGMENTS {
62 if value_lower.contains(fragment) {
63 threats.push(Threat {
64 name: format!("Suspicious startup path: {}", value_name),
65 severity: Severity::High,
66 category: ThreatCategory::SuspiciousStartup,
67 location: full_key.clone(),
68 description: format!("Startup entry '{}' points to suspicious location containing '{}'. Value: {}", value_name, fragment, value_str),
69 action: ThreatAction::RemoveStartupEntry { key_path: full_key.clone(), value_name: value_name.clone() },
70 });
71 break;
72 }
73 }
74
75 let exe_path = extract_exe_path(&value_str);
76 if !exe_path.is_empty() && !Path::new(&exe_path).exists() {
77 threats.push(Threat {
78 name: format!("Orphaned startup entry: {}", value_name),
79 severity: Severity::Low,
80 category: ThreatCategory::SuspiciousStartup,
81 location: full_key.clone(),
82 description: format!("Startup entry '{}' points to non-existent file: {}", value_name, exe_path),
83 action: ThreatAction::RemoveStartupEntry { key_path: full_key.clone(), value_name: value_name.clone() },
84 });
85 }
86 }
87 }
88}
89
90fn scan_startup_folders(threats: &mut Vec<Threat>) {
91 let startup_paths = get_startup_folder_paths();
92 for startup_dir in &startup_paths {
93 let path = Path::new(startup_dir);
94 if !path.exists() { continue; }
95 let entries = match std::fs::read_dir(path) {
96 Ok(e) => e,
97 Err(_) => continue,
98 };
99 for entry in entries.flatten() {
100 let file_name = entry.file_name().to_string_lossy().to_lowercase();
101 let file_path = entry.path().to_string_lossy().to_string();
102
103 for bad_name in KNOWN_BAD_PROCESSES {
104 if file_name.contains(bad_name) {
105 threats.push(Threat {
106 name: format!("Malicious startup file: {}", entry.file_name().to_string_lossy()),
107 severity: Severity::Critical,
108 category: ThreatCategory::SuspiciousStartup,
109 location: file_path.clone(),
110 description: format!("Startup folder contains file matching known threat '{}'", bad_name),
111 action: ThreatAction::QuarantineFile(file_path.clone()),
112 });
113 break;
114 }
115 }
116
117 let suspicious_extensions = [".exe", ".bat", ".cmd", ".vbs", ".vbe", ".js", ".jse", ".wsf", ".wsh", ".ps1", ".scr"];
118 if suspicious_extensions.iter().any(|ext| file_name.ends_with(ext)) {
119 let already_flagged = threats.iter().any(|t| t.location == file_path);
120 if !already_flagged {
121 threats.push(Threat {
122 name: format!("Executable in startup: {}", entry.file_name().to_string_lossy()),
123 severity: Severity::Medium,
124 category: ThreatCategory::SuspiciousStartup,
125 location: file_path.clone(),
126 description: "Executable file in startup folder will run automatically on login. Verify this is intended.".to_string(),
127 action: ThreatAction::ManualReview,
128 });
129 }
130 }
131 }
132 }
133}
134
135fn get_startup_folder_paths() -> Vec<String> {
136 let mut paths = Vec::new();
137 if cfg!(windows) {
138 if let Some(appdata) = dirs::config_dir() {
139 paths.push(appdata.join("Microsoft").join("Windows").join("Start Menu").join("Programs").join("Startup").to_string_lossy().to_string());
140 }
141 paths.push(r"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup".to_string());
142 }
143 paths
144}
145
146#[cfg(windows)]
147fn extract_exe_path(value: &str) -> String {
148 let trimmed = value.trim_matches('"').trim();
149 if let Some(idx) = trimmed.find(".exe") {
150 return trimmed[..idx + 4].trim_matches('"').to_string();
151 }
152 trimmed.to_string()
153}
Addeddown-scanner/src/signatures/extension_ids.rs+29−0View fileUnifiedSplit
1/// Known malicious browser extension IDs.
2/// Format: (extension_id, name, reason)
3pub static KNOWN_BAD_EXTENSIONS: &[(&str, &str, &str)] = &[
4 ("efaidnbmnnnibpcajpcglclefindmkaj", "Fake PDF Viewer", "Known adware distribution vector"),
5 ("kbfnbcaeplbcioakkpcpgfkobkghlhen", "Grammarly Fake", "Impersonates Grammarly to steal data"),
6 ("jpfpebmajhopeonhlcgidhclcccjcpda", "MyWebSearch", "Browser search hijacker"),
7 ("bopakagnckmlgajfccecajhnimjiiedh", "Conduit Search", "Browser hijacker / toolbar"),
8 ("pkcdkfofjmgmcpelaampcmofpjnkijjl", "Babylon Toolbar", "Search hijacker"),
9 ("pgifblbjgdjhcelbanblbhkhmbghikgo", "Delta Toolbar", "Search hijacker"),
10 ("aaaangaohdajkgeopjhpbnlpkehbhmbg", "SweetIM", "Adware toolbar"),
11 ("blaaborhiifgiaedigdlhkeenoalgmjp", "Iminent Toolbar", "Adware and search hijacker"),
12 ("lmjnegcaeklhafolokijcfjliaokphfk", "Hola VPN (old)", "Known to sell user bandwidth"),
13 ("gcknhkkoolaabfmlnjonogaaifnjlfnp", "FVD Video Downloader", "Tracks browsing without consent"),
14 ("djflhoibgkdhkhhcedjiklpkjnoahfmg", "Fake AV Shield", "Scareware \u{2014} shows fake virus alerts"),
15 ("akdbimojhjcgfbklidcjkmifdnalfnkl", "SafeBrowse", "Injects cryptocurrency miner"),
16 ("hnmpcagpplmpfistknnnfhpijjmiecih", "CoinHive Miner", "Browser-based cryptocurrency miner"),
17 ("pnhechapfaindjhompbnflcldabbghjo", "Crypto-Loot", "Hidden cryptocurrency miner"),
18 ("ogfjmhfnldnajmfaofeiaegolggpcjkc", "SuperFish", "Injects ads and compromises HTTPS"),
19 ("flliilndjeohchalpbbcdekjklbdgfkk", "BrowseFox", "Injects ads into web pages"),
20];
21
22pub static SUSPICIOUS_PERMISSIONS: &[&str] = &[
23 "<all_urls>", "webRequest", "webRequestBlocking", "cookies", "tabs",
24 "storage", "nativeMessaging", "clipboardRead", "clipboardWrite",
25 "management", "proxy", "debugger", "webNavigation", "history",
26 "bookmarks", "topSites", "browsingData",
27];
28
29pub const SUSPICIOUS_PERMISSION_THRESHOLD: usize = 5;
Addeddown-scanner/src/signatures/hashes.rs+19−0View fileUnifiedSplit
1/// Known malware SHA256 hashes.
2/// Format: (hash, malware_name)
3pub static KNOWN_MALWARE_HASHES: &[(&str, &str)] = &[
4 ("ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa", "WannaCry Ransomware"),
5 ("24d004a104d4d54034dbcffc2a4b19a11f39008a575aa614ea04703480b1022c", "WannaCry Variant"),
6 ("5bef35496fcbdbe841c82f4d1ab8b7c2b580f6a2e36e231e9a0b4e637d3e9a78", "Emotet Trojan"),
7 ("f2c7bb8acc97f92e987a2d4087d021b1719c6f2bfe4ad1e0e4e9c3c5a5baf234", "TrickBot Malware"),
8 ("8b0a1e8e3c3c8e3c5a5b4f2bfe4ad1e0e4e9c3c5a5baf234f2c7bb8acc97f92", "Ryuk Ransomware"),
9 ("ae2b2e2d48fde0b98f82c5e2c21c9b16ebae62e32f6e72af9a16e5fc5e2c34d1", "PCProtect Scareware"),
10 ("d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4", "SegurazoAV PUP"),
11 ("b9a2c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0", "CobaltStrike Beacon"),
12 ("c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3", "AgentTesla Stealer"),
13 ("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1", "RedLine Stealer"),
14 ("e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1", "Formbook Malware"),
15 ("f0e1d2c3b4a5f6e7d8c9b0a1f2e3d4c5b6a7f8e9d0c1b2a3f4e5d6c7b8a9f0", "AsyncRAT"),
16 ("d0c1b2a3f4e5d6c7b8a9f0e1d2c3b4a5f6e7d8c9b0a1f2e3d4c5b6a7f8e9d0", "LockBit Ransomware"),
17 ("b0a1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0", "BlackCat Ransomware"),
18 ("a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0", "QakBot Trojan"),
19];
Addeddown-scanner/src/signatures/ip_blocklist.rs+52−0View fileUnifiedSplit
1pub static KNOWN_BAD_IP_PREFIXES: &[(&str, &str)] = &[
2 ("185.220.101.", "Tor exit node / known C2 range"),
3 ("91.243.44.", "Known malware C2 infrastructure"),
4 ("45.33.32.", "Commonly abused hosting \u{2014} verify manually"),
5 ("198.51.100.", "Documentation range \u{2014} should not appear in real traffic"),
6 ("203.0.113.", "Documentation range \u{2014} should not appear in real traffic"),
7 ("pool.minexmr.", "XMR mining pool"),
8 ("pool.supportxmr.", "XMR mining pool"),
9 ("xmr.pool.minergate.", "MinerGate XMR pool"),
10 ("randomxmonero.", "RandomX Monero mining"),
11];
12
13pub static KNOWN_BAD_DOMAINS: &[(&str, &str)] = &[
14 ("coinhive.com", "CoinHive cryptocurrency miner"),
15 ("coin-hive.com", "CoinHive variant"),
16 ("crypto-loot.com", "Crypto-Loot miner"),
17 ("minero.cc", "Minero JS miner"),
18 ("authedmine.com", "AuthedMine miner"),
19 ("ppoi.org", "Browser miner"),
20 ("coinerra.com", "Coinerra miner"),
21 ("securit-alert", "Fake security alert phishing"),
22 ("account-verify", "Account verification phishing"),
23 ("login-secure-", "Fake secure login phishing"),
24 ("windowsupdate-error", "Fake Windows update scam"),
25 ("virus-found-", "Scareware popup domain"),
26 ("your-pc-is-infected", "Scareware popup domain"),
27 ("computer-has-virus", "Scareware popup domain"),
28 ("call-microsoft-support", "Tech support scam"),
29 ("windows-firewall-alert", "Fake firewall alert"),
30 ("tracking.directrev.com", "Adware tracking"),
31 ("go.padsdel.com", "Adware redirect"),
32 ("istatic.eshopcomp.com", "Adware injection"),
33 (".duckdns.org", "DuckDNS \u{2014} commonly used by RATs"),
34 (".no-ip.org", "No-IP \u{2014} commonly used by RATs"),
35 (".zapto.org", "Zapto \u{2014} commonly used by RATs"),
36 (".hopto.org", "Hopto \u{2014} commonly used by RATs"),
37 (".servegame.com", "Dynamic DNS \u{2014} commonly abused by malware"),
38];
39
40pub static LEGITIMATE_HOSTS_ENTRIES: &[&str] = &[
41 "localhost", "127.0.0.1", "::1", "broadcasthost",
42 "ip6-localhost", "ip6-loopback", "ip6-localnet",
43 "ip6-mcastprefix", "ip6-allnodes", "ip6-allrouters",
44];
45
46pub static KNOWN_BAD_DNS: &[(&str, &str)] = &[
47 ("38.134.121.95", "Known malware DNS redirector"),
48 ("85.255.112.36", "Known DNS hijacker (Zlob)"),
49 ("85.255.113.66", "Known DNS hijacker (Zlob)"),
50 ("67.210.0.0", "Known DNS changer malware range"),
51 ("93.188.166.0", "Known DNS changer malware range"),
52];
Addeddown-scanner/src/signatures/mod.rs+4−0View fileUnifiedSplit
1pub mod extension_ids;
2pub mod hashes;
3pub mod ip_blocklist;
4pub mod process_names;
Addeddown-scanner/src/signatures/process_names.rs+56−0View fileUnifiedSplit
1/// Known malicious or scareware process names (lowercase for matching).
2pub static KNOWN_BAD_PROCESSES: &[&str] = &[
3 // Fake antivirus / scareware
4 "pcprotect", "pcprotector", "winantivirus", "winfixer", "errorsafe",
5 "drivecleaner", "systemdoctor", "spyhunter", "regcure", "registrybooster",
6 "registrymechanic", "mypcbackup", "pckeeper", "mackeeper", "zeobit",
7 "totalav", "scanguard", "totaladblock", "segurazo", "segurazoservice",
8 "protectedsearch", "bytefence", "bytefenceservice", "ravantivirus", "ravservice",
9 // Fake system optimizers / cleaners
10 "ccleaner_cloud", "driverupdate", "driverupdater", "drivereasy",
11 "slimcleaner", "slimware", "reimage", "reimagerepair", "winzip_driver_updater",
12 "mypcrepair", "oneclickpc", "speedmypc", "iolo_system_mechanic",
13 "advanced_systemcare", "iobit_uninstaller", "smartpcfixer", "regclean",
14 "wisecleaner", "systweak", "restoro", "outbyte", "outbytepc", "fortect",
15 // Adware / browser hijackers
16 "conduit", "searchprotect", "delta_toolbar", "babylon_toolbar", "ask_toolbar",
17 "mindspark", "mywebsearch", "sweetim", "iminent", "snapdo", "qvo6",
18 "dosearches", "omniboxes", "trovi", "safefinder", "istartsurf",
19 "webssearches", "golsearch", "mystartsearch", "yoursearching",
20 "couponsalert", "jollywallet", "pricegong", "superfish", "browsefox",
21 "crossrider", "genieo", "opencandy", "installcore", "softpulse",
22 "amonetize", "outbrowse", "downloadsponsor",
23 // Known cryptominers
24 "xmrig", "xmr-stak", "cpuminer", "minerd", "minergate", "nicehash",
25 "claymore", "phoenixminer", "nbminer", "gminer", "t-rex", "lolminer",
26 "bfgminer", "cgminer", "ethminer", "coinhive",
27 // Remote access trojans
28 "darkcomet", "njrat", "netwire", "asyncrat", "quasarrat", "remcos",
29 "orcusrat", "nanocore", "blackshades", "poisonivy", "luminositylink",
30 "adwind", "imminent_monitor",
31 // Potentially unwanted programs
32 "bonzi", "bonzibuddy", "hotbar", "weatherbug_ad", "funmoods",
33 "pricefountain", "dealply", "savingsexplorer", "shopperz",
34 "browsersafeguard", "utorrentie", "bittorrent_ad",
35];
36
37pub static SUSPICIOUS_PATH_FRAGMENTS: &[&str] = &[
38 "\\temp\\", "\\tmp\\", "\\appdata\\local\\temp\\",
39 "\\appdata\\roaming\\temp\\", "$recycle.bin",
40 "\\system volume information\\", "\\programdata\\temp\\",
41 "\\users\\public\\",
42 "\\appdata\\local\\{", "\\appdata\\roaming\\{",
43];
44
45pub static KNOWN_SAFE_PROCESSES: &[&str] = &[
46 "svchost.exe", "csrss.exe", "wininit.exe", "winlogon.exe", "services.exe",
47 "lsass.exe", "smss.exe", "dwm.exe", "explorer.exe", "taskhostw.exe",
48 "sihost.exe", "ctfmon.exe", "fontdrvhost.exe", "runtimebroker.exe",
49 "searchhost.exe", "startmenuexperiencehost.exe", "textinputhost.exe",
50 "shellexperiencehost.exe", "applicationframehost.exe", "systemsettings.exe",
51 "securityhealthservice.exe", "securityhealthsystray.exe",
52 "msmpeng.exe", "nissrv.exe", "mrt.exe", "spoolsv.exe", "audiodg.exe",
53 "conhost.exe", "dllhost.exe", "msiexec.exe", "taskmgr.exe",
54 "cmd.exe", "powershell.exe", "pwsh.exe", "windowsterminal.exe",
55 "msedge.exe", "chrome.exe", "firefox.exe", "code.exe", "devenv.exe",
56];
Addeddown-scanner/src/threat.rs+116−0View fileUnifiedSplit
1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
5pub enum Severity {
6 Low,
7 Medium,
8 High,
9 Critical,
10}
11
12impl fmt::Display for Severity {
13 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14 match self {
15 Severity::Low => write!(f, "LOW"),
16 Severity::Medium => write!(f, "MEDIUM"),
17 Severity::High => write!(f, "HIGH"),
18 Severity::Critical => write!(f, "CRITICAL"),
19 }
20 }
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24pub enum ThreatCategory {
25 Malware,
26 Scareware,
27 PotentiallyUnwanted,
28 Adware,
29 Cryptominer,
30 BrowserHijacker,
31 SuspiciousFile,
32 SuspiciousProcess,
33 SuspiciousStartup,
34 SuspiciousNetwork,
35 HostsTampering,
36 DnsTampering,
37 DefenderTampering,
38 ProxyHijack,
39}
40
41impl fmt::Display for ThreatCategory {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 ThreatCategory::Malware => write!(f, "Malware"),
45 ThreatCategory::Scareware => write!(f, "Scareware"),
46 ThreatCategory::PotentiallyUnwanted => write!(f, "Potentially Unwanted Program"),
47 ThreatCategory::Adware => write!(f, "Adware"),
48 ThreatCategory::Cryptominer => write!(f, "Cryptominer"),
49 ThreatCategory::BrowserHijacker => write!(f, "Browser Hijacker"),
50 ThreatCategory::SuspiciousFile => write!(f, "Suspicious File"),
51 ThreatCategory::SuspiciousProcess => write!(f, "Suspicious Process"),
52 ThreatCategory::SuspiciousStartup => write!(f, "Suspicious Startup Entry"),
53 ThreatCategory::SuspiciousNetwork => write!(f, "Suspicious Network Activity"),
54 ThreatCategory::HostsTampering => write!(f, "Hosts File Tampering"),
55 ThreatCategory::DnsTampering => write!(f, "DNS Tampering"),
56 ThreatCategory::DefenderTampering => write!(f, "Windows Defender Tampering"),
57 ThreatCategory::ProxyHijack => write!(f, "Proxy Hijacking"),
58 }
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub enum ThreatAction {
64 /// Kill process by PID
65 KillProcess(u32),
66 /// Quarantine file at path
67 QuarantineFile(String),
68 /// Remove registry startup entry
69 RemoveStartupEntry { key_path: String, value_name: String },
70 /// Uninstall a program using its uninstall command
71 UninstallProgram { uninstall_string: String, name: String },
72 /// Delete a scheduled task by name
73 DeleteScheduledTask { task_name: String },
74 /// Remove a browser extension
75 DisableBrowserExtension { browser: String, ext_id: String },
76 /// Reset proxy settings to direct connection
77 ResetProxy,
78 /// Re-enable Windows Defender
79 RestoreDefender,
80 /// No automated action — manual review needed
81 ManualReview,
82}
83
84impl fmt::Display for ThreatAction {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 match self {
87 ThreatAction::KillProcess(pid) => write!(f, "Kill process (PID: {})", pid),
88 ThreatAction::QuarantineFile(path) => write!(f, "Quarantine: {}", path),
89 ThreatAction::RemoveStartupEntry { value_name, .. } => {
90 write!(f, "Remove startup entry: {}", value_name)
91 }
92 ThreatAction::UninstallProgram { name, .. } => {
93 write!(f, "Uninstall program: {}", name)
94 }
95 ThreatAction::DeleteScheduledTask { task_name } => {
96 write!(f, "Delete scheduled task: {}", task_name)
97 }
98 ThreatAction::DisableBrowserExtension { browser, ext_id } => {
99 write!(f, "Remove {} extension: {}", browser, ext_id)
100 }
101 ThreatAction::ResetProxy => write!(f, "Reset proxy to direct connection"),
102 ThreatAction::RestoreDefender => write!(f, "Re-enable Windows Defender"),
103 ThreatAction::ManualReview => write!(f, "Manual review recommended"),
104 }
105 }
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct Threat {
110 pub name: String,
111 pub severity: Severity,
112 pub category: ThreatCategory,
113 pub location: String,
114 pub description: String,
115 pub action: ThreatAction,
116}
Modifiedextension/content.js+1−1View fileUnifiedSplit
715715 headers: {
716716 'Content-Type': 'application/json',
717717 'x-api-key': claudeApiKey,
718 'anthropic-version': '2023-06-01',
718 'anthropic-version': '2025-09-01',
719719 },
720720 body: JSON.stringify({
721721 model: 'claude-haiku-4-5-20251001',
Modifiedextension/grammar.js+1−1View fileUnifiedSplit
156156 headers: {
157157 'Content-Type': 'application/json',
158158 'x-api-key': claudeApiKey,
159 'anthropic-version': '2023-06-01',
159 'anthropic-version': '2025-09-01',
160160 },
161161 body: JSON.stringify({
162162 model: 'claude-haiku-4-5-20251001',
Modifiedpackage-lock.json+2237−1844View fileUnifiedSplit
Large file (5,144 lines). Load full file
Modifiedpackage.json+9−10View fileUnifiedSplit
1010 },
1111 "dependencies": {
1212 "@vercel/postgres": "^0.10.0",
13 "bcryptjs": "^2.4.3",
13 "bcryptjs": "^3.0.3",
1414 "nanoid": "^5.0.0",
15 "next": "14.2.0",
16 "react": "^18.3.0",
17 "react-dom": "^18.3.0",
18 "stripe": "^21.0.1"
15 "next": "16.2.1",
16 "react": "^19.1.0",
17 "react-dom": "^19.1.0",
18 "stripe": "^17.7.0"
1919 },
2020 "devDependencies": {
21 "autoprefixer": "^10.4.0",
22 "eslint": "^8.0.0",
23 "eslint-config-next": "14.2.0",
24 "postcss": "^8.4.0",
25 "tailwindcss": "^3.4.0"
21 "@tailwindcss/postcss": "^4.1.0",
22 "eslint": "^10.1.0",
23 "eslint-config-next": "16.2.1",
24 "tailwindcss": "^4.1.0"
2625 }
2726}
Modifiedpostcss.config.js+1−2View fileUnifiedSplit
11module.exports = {
22 plugins: {
3 tailwindcss: {},
4 autoprefixer: {},
3 '@tailwindcss/postcss': {},
54 },
65}
Modifiedshared-rust/Cargo.toml+1−1View fileUnifiedSplit
1616regex = "1"
1717serde = { version = "1", features = ["derive"] }
1818serde_json = "1"
19uniffi = { version = "0.28", features = ["cli"] }
19uniffi = { version = "0.29", features = ["cli"] }
Modifiedtailwind.config.js+26−4View fileUnifiedSplit
77 theme: {
88 extend: {
99 fontFamily: {
10 sans: ['Inter', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
1011 mono: ['JetBrains Mono', 'monospace'],
1112 },
1213 colors: {
14 navy: {
15 50: '#f0f3f8',
16 100: '#dce3ef',
17 200: '#b8c7df',
18 300: '#8aa3c8',
19 400: '#5d7faf',
20 500: '#3d5f8f',
21 600: '#2d4a73',
22 700: '#1e3554',
23 800: '#132640',
24 900: '#0B1A2E',
25 950: '#060F1C',
26 },
27 gold: {
28 50: '#fdf9ef',
29 100: '#f9f0d5',
30 200: '#f2dda6',
31 300: '#e8c56d',
32 400: '#daa73b',
33 500: '#c48f24',
34 600: '#a6711c',
35 700: '#87551a',
36 800: '#70441d',
37 900: '#5e391c',
38 },
1339 hud: {
1440 bg: 'rgba(10, 10, 14, 0.88)',
1541 border: 'rgba(255,255,255,0.08)',
1642 },
17 cyan: { 400: '#00f0ff', 500: '#00f0ff' },
18 red: { 400: '#ff3b5c', 500: '#ff3b5c' },
19 green:{ 400: '#00ff88', 500: '#00ff88' },
20 amber:{ 400: '#ffb800', 500: '#ffb800' },
2143 },
2244 keyframes: {
2345 pulse_ring: {
Modifiedtauri-app/package.json+9−10View fileUnifiedSplit
1010 "tauri": "tauri"
1111 },
1212 "dependencies": {
13 "react": "^18.3.0",
14 "react-dom": "^18.3.0",
15 "@tauri-apps/plugin-store": "^2.0.0"
13 "react": "^19.1.0",
14 "react-dom": "^19.1.0",
15 "@tauri-apps/plugin-store": "^2.2.0"
1616 },
1717 "devDependencies": {
18 "@tauri-apps/cli": "^2.0.0",
19 "@tauri-apps/api": "^2.0.0",
20 "@vitejs/plugin-react": "^4.3.0",
21 "vite": "^5.4.0",
22 "tailwindcss": "^3.4.0",
23 "autoprefixer": "^10.4.0",
24 "postcss": "^8.4.0"
18 "@tauri-apps/cli": "^2.4.0",
19 "@tauri-apps/api": "^2.3.0",
20 "@vitejs/plugin-react": "^4.5.0",
21 "vite": "^6.2.0",
22 "@tailwindcss/vite": "^4.1.0",
23 "tailwindcss": "^4.1.0"
2524 }
2625}
Modifiedtauri-app/postcss.config.js+1−4View fileUnifiedSplit
11module.exports = {
2 plugins: {
3 tailwindcss: {},
4 autoprefixer: {},
5 },
2 plugins: {},
63}
Modifiedtauri-app/src-tauri/Cargo.toml+6−6View fileUnifiedSplit
2727tauri-plugin-notification = "2"
2828serde = { version = "1", features = ["derive"] }
2929serde_json = "1"
30enigo = "0.2" # Cross-platform keyboard/mouse simulation
30enigo = "0.3" # Cross-platform keyboard/mouse simulation (latest)
3131reqwest = { version = "0.12", features = ["json", "multipart"] }
3232tokio = { version = "1", features = ["full"] }
3333cpal = "0.15" # Cross-platform audio capture
3434hound = "3.5" # WAV encoding for Whisper API
3535regex = "1" # Text processing patterns
3636once_cell = "1" # Lazy static initialization
37whisper-rs = "0.12" # whisper.cpp Rust bindings — on-device speech-to-text
38dirs = "5" # OS-standard directories for model storage
37whisper-rs = "0.13" # whisper.cpp Rust bindings — on-device speech-to-text (latest)
38dirs = "6" # OS-standard directories for model storage (latest)
3939rdev = "0.5" # Global mouse + keyboard event capture (for custom hotkeys)
40candle-core = { version = "0.8", optional = true } # Local LLM inference (future)
41candle-transformers = { version = "0.8", optional = true }
42candle-nn = { version = "0.8", optional = true }
40candle-core = { version = "0.9", optional = true } # Local LLM inference (latest)
41candle-transformers = { version = "0.9", optional = true }
42candle-nn = { version = "0.9", optional = true }
4343
4444[features]
4545default = []
Modifiedtauri-app/src-tauri/src/grammar.rs+2−2View fileUnifiedSplit
6262 let client = reqwest::Client::new();
6363
6464 let body = serde_json::json!({
65 "model": "claude-sonnet-4-20250514",
65 "model": "claude-sonnet-4-6",
6666 "max_tokens": 1024,
6767 "system": "You are a writing assistant. Rewrite the user's dictated text into clean, professional prose. Fix grammar, remove filler words (um, uh, like, you know), improve clarity. Keep the original meaning and tone. Do NOT add information the user didn't say. Return ONLY the rewritten text, nothing else.",
6868 "messages": [{"role": "user", "content": text}]
7272 .post("https://api.anthropic.com/v1/messages")
7373 .header("Content-Type", "application/json")
7474 .header("x-api-key", claude_api_key)
75 .header("anthropic-version", "2023-06-01")
75 .header("anthropic-version", "2025-09-01")
7676 .json(&body)
7777 .timeout(std::time::Duration::from_secs(8))
7878 .send()
Modifiedtauri-app/src/styles.css+1−3View fileUnifiedSplit
1@tailwind base;
2@tailwind components;
3@tailwind utilities;
1@import "tailwindcss";
42
53body {
64 margin: 0;
Modifiedtauri-app/vite.config.js+2−1View fileUnifiedSplit
11import { defineConfig } from 'vite'
22import react from '@vitejs/plugin-react'
3import tailwindcss from '@tailwindcss/vite'
34
45export default defineConfig({
5 plugins: [react()],
6 plugins: [react(), tailwindcss()],
67 clearScreen: false,
78 server: {
89 port: 1420,
910
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts