Compare commits
209 Commits
feature/re
...
daeea5f26c
| Author | SHA1 | Date | |
|---|---|---|---|
| daeea5f26c | |||
| deecac745d | |||
| 327dc329ab | |||
| 0499f06ff8 | |||
| e7233d69a3 | |||
| e2b8fcee21 | |||
| 6e2d99bd23 | |||
| 6ec260c023 | |||
| b6d00207ff | |||
|
|
742c25c96e | ||
| 34aad7639f | |||
|
|
1191123602 | ||
|
|
887a672e7d | ||
| e5dc77cb96 | |||
| 5ec20ac9dd | |||
| 8613d57f6f | |||
| 7e2e3a5882 | |||
| abfb0a18b0 | |||
| 5c6f7dca5e | |||
| 2b13299657 | |||
| 0be47f23ef | |||
| 1534a2f9a0 | |||
| af4ad0aea3 | |||
| 6bd416657c | |||
| 90d4f386aa | |||
| 7bffbfbb89 | |||
| ce52a62ff1 | |||
| e0a120b74e | |||
| c077d58372 | |||
| 223dc861c2 | |||
| 96ad4b7059 | |||
| d5db062136 | |||
| db5395ebe9 | |||
| 4b2afc1a5e | |||
| 2ae7d6a0ac | |||
| dcc852b12d | |||
| d39fef2a23 | |||
| af60f8231f | |||
| b7752d3d7f | |||
| a186551ce3 | |||
| e2028fe6f8 | |||
| c32a4101e6 | |||
| a5dfdbc75c | |||
| e644ca8526 | |||
| 786049b115 | |||
| 386a115039 | |||
| 54fa7a3f4f | |||
| 30dbd39fee | |||
| 7a377d882d | |||
| 741b259760 | |||
| a771d4ed11 | |||
| b3f51aad0f | |||
| 3f94aefa57 | |||
| 6125ba15d9 | |||
| a5b87e324d | |||
| 2484075f6f | |||
| 4bb5dd937b | |||
| cae7b63481 | |||
| 773a3540ba | |||
| 00dc60f460 | |||
| 2011064af3 | |||
| 93e9dcc650 | |||
| c40a71e452 | |||
| 90f9d9eda1 | |||
| c37816736b | |||
| 28d6b7646d | |||
| 4d80bd96d1 | |||
| 14e7354ba5 | |||
| a86df117d2 | |||
| 0a7cd6b778 | |||
| 347b2d30a9 | |||
| 63089c45c9 | |||
| a9bcbc2580 | |||
| 48f1b4b612 | |||
| 1865ae705b | |||
| f15862440e | |||
| 52a02c48f3 | |||
| 01b3fee503 | |||
| 9143eb6262 | |||
| db4e3c25a5 | |||
| c83dd47d45 | |||
| 1307431afa | |||
| 924f326e7f | |||
| 50140ac88c | |||
| 597a94a584 | |||
| 7b068b1439 | |||
| 31afc61a55 | |||
| 821435594b | |||
| 89300e7ac7 | |||
| 7a2e41c28c | |||
| fb38fdeef7 | |||
| fd6c96513d | |||
| 41eac14c33 | |||
| cd50117aaf | |||
| 749a472089 | |||
| 2f0bc654a1 | |||
| 06c53ee324 | |||
| a8b4a7c324 | |||
| 936ea49b33 | |||
| 056e36aeac | |||
| ebad88de57 | |||
| 21417c6c20 | |||
| be23c91ea4 | |||
| 26df2c47b9 | |||
| b0db273e1e | |||
| a92d2d3f2c | |||
| 9694b4d521 | |||
| 4eb0d208f2 | |||
| 8944432941 | |||
| 245454b155 | |||
| a00f1b0c3e | |||
| acc6308352 | |||
| 5c59e7c57e | |||
| af31c3a60c | |||
| 94585fe426 | |||
| 0c136cd2ee | |||
| 98ba8bc060 | |||
| d37cc238d2 | |||
| 492fbbf4c9 | |||
| b28152a358 | |||
| f58f5c58b7 | |||
| 80c89a8599 | |||
| fd64877ba7 | |||
| 74a8fa5968 | |||
| 2088bd9f0d | |||
| 51f96e8802 | |||
| 96285e8693 | |||
| fd00f2d592 | |||
| 39fb617965 | |||
| 0fc1c5986e | |||
| 1cd25f6f41 | |||
| a3b9ab9f41 | |||
| a6180b8ebf | |||
| b8403305d7 | |||
| e226d2857e | |||
| c4fdb5a233 | |||
| c44a01f5dd | |||
| ed16744db0 | |||
| 1bac987009 | |||
| 41b7648133 | |||
| a8692a9074 | |||
| 17865c30fc | |||
| 002a3ff69b | |||
| dfcc3cefef | |||
| 8ec777245a | |||
| 6ca389135a | |||
| 9c820c16fa | |||
| cb300a193c | |||
| f732848c24 | |||
| 2b05bf6130 | |||
| afd5eb2a2c | |||
| 4bf151ca7b | |||
| 74890d51ec | |||
| a173c70633 | |||
| d2e375df8a | |||
| 6a135ac111 | |||
| 975adda092 | |||
| 7660cb4a16 | |||
| 5b8813af4d | |||
| c957ef33ef | |||
| 68153cf9b6 | |||
| 273342ee9f | |||
| a80ea236ba | |||
| 3358cecdcc | |||
| fe3b5b0382 | |||
| 0a7f3368a6 | |||
| 3eb621a8b7 | |||
| 4220b8f57c | |||
| 4886c8cc2a | |||
| 5a31946083 | |||
| 71c9ddce9e | |||
| e695743149 | |||
| 5995511011 | |||
| b99f8512e4 | |||
| 68d9836245 | |||
| dd8e45de80 | |||
| 32888ea9d4 | |||
| ac4ceb65c0 | |||
| 392c42710c | |||
| 046175af3a | |||
| 6bb00601b7 | |||
| 996dd515b1 | |||
| f190f7813f | |||
| a3fe1b9a9b | |||
| d13d4e4909 | |||
| 8d975c1b44 | |||
| 6eaba02b71 | |||
| f5acf9f453 | |||
| 8a094529ab | |||
| 6f6a77f8e4 | |||
| 100a491ac6 | |||
| a18157b5fa | |||
| 43c116f0c6 | |||
| ea48061389 | |||
| 232f463325 | |||
| d033dbe8a2 | |||
| 148ac75a25 | |||
| 2937c29f07 | |||
| fdd0bb0c1f | |||
| 5abf9ba670 | |||
| f01d9d5538 | |||
| 733d87f20e | |||
| eae9d7f644 | |||
| dd5c5afd4b | |||
| 72105233a2 | |||
| d0dbfed5ec | |||
| 04bdac0448 | |||
| 7326fbb05c | |||
| c9eba69753 |
347
.claude/AUTO_CONTEXT_SYSTEM.md
Normal file
347
.claude/AUTO_CONTEXT_SYSTEM.md
Normal file
@@ -0,0 +1,347 @@
|
||||
# Automatic Context Loading System
|
||||
|
||||
## The Problem
|
||||
|
||||
Claude instances don't proactively review previous work before starting. User must say "review previous work first" which defeats the purpose of the context recovery system.
|
||||
|
||||
**Example failure:**
|
||||
```
|
||||
User: "Look at the Dataforth DFWDS folders"
|
||||
Claude: "What work have we done on this?" # WRONG - should check CONTEXT.md first
|
||||
```
|
||||
|
||||
## The Solution: Tiered Hint System
|
||||
|
||||
### Tier 1: Quick Hints (CONTEXT.md)
|
||||
- High-level overview (infrastructure, current state, anti-patterns)
|
||||
- Fast to read (~30 seconds)
|
||||
- Points to detailed resources
|
||||
- **Location:** [project-root]/CONTEXT.md
|
||||
|
||||
### Tier 2: Detailed Resources
|
||||
- Recent session logs (full commands, decisions)
|
||||
- Implementation plans, technical specs
|
||||
- Pointed to by CONTEXT.md
|
||||
|
||||
### Tier 3: Deep Archive
|
||||
- Historical session logs
|
||||
- Git history
|
||||
- Full technical documentation
|
||||
|
||||
## Automatic Loading Rules
|
||||
|
||||
### Rule 1: Project Mention Detection
|
||||
|
||||
**When user message contains project keywords, auto-load CONTEXT.md:**
|
||||
|
||||
| Keywords | Load | Read Before Responding |
|
||||
|----------|------|------------------------|
|
||||
| "GuruRMM", "tunnel", "agents", "rmm-api" | projects/msp-tools/guru-rmm/CONTEXT.md | Yes |
|
||||
| "Dataforth", "DFWDS", "testdatadb", "AD2", "VASLOG" | projects/dataforth-dos/CONTEXT.md | Yes |
|
||||
| "ClaudeTools API", "work tracking" | CONTEXT.md (root) | Yes |
|
||||
|
||||
**Implementation:**
|
||||
```python
|
||||
import re
|
||||
|
||||
PROJECT_PATTERNS = {
|
||||
'gururmm': {
|
||||
'keywords': r'\b(GuruRMM|tunnel|rmm-api|gururmm|agent.*status)\b',
|
||||
'context_file': 'projects/msp-tools/guru-rmm/CONTEXT.md',
|
||||
},
|
||||
'dataforth': {
|
||||
'keywords': r'\b(Dataforth|DFWDS|testdatadb|AD2|AD1|VASLOG|SCMVAS|SCMHVAS)\b',
|
||||
'context_file': 'projects/dataforth-dos/CONTEXT.md',
|
||||
},
|
||||
'claudetools': {
|
||||
'keywords': r'\b(ClaudeTools API|work tracking|claudetools database)\b',
|
||||
'context_file': 'CONTEXT.md',
|
||||
},
|
||||
}
|
||||
|
||||
def detect_project(user_message):
|
||||
for project, config in PROJECT_PATTERNS.items():
|
||||
if re.search(config['keywords'], user_message, re.IGNORECASE):
|
||||
return config['context_file']
|
||||
return None
|
||||
```
|
||||
|
||||
### Rule 2: Continuation Context Detection
|
||||
|
||||
**When user says "continue", "let's work on", "back to", auto-check for active project:**
|
||||
|
||||
```
|
||||
User: "Let's continue working on the tunnel"
|
||||
Claude: [Detects "tunnel" → reads GuruRMM CONTEXT.md]
|
||||
"I see tunnel Phase 1 is complete (v0.6.0). Phase 2 is channel implementation..."
|
||||
```
|
||||
|
||||
### Rule 3: Uncertainty Threshold
|
||||
|
||||
**When Claude's certainty < 95%, auto-check hints:**
|
||||
|
||||
```python
|
||||
def should_check_hints(user_message, current_knowledge):
|
||||
"""Check if we should read CONTEXT.md before responding"""
|
||||
|
||||
# Rule 1: Project keyword mentioned
|
||||
if detect_project(user_message):
|
||||
return True
|
||||
|
||||
# Rule 2: Continuation words
|
||||
if re.search(r'\b(continue|back to|work on|finish|resume)\b', user_message, re.I):
|
||||
return True
|
||||
|
||||
# Rule 3: Infrastructure questions
|
||||
if re.search(r'\b(server|database|deploy|credentials|IP|password)\b', user_message, re.I):
|
||||
return True
|
||||
|
||||
# Rule 4: Reference to past work
|
||||
if re.search(r'\b(last time|previous|recent|we did|earlier)\b', user_message, re.I):
|
||||
return True
|
||||
|
||||
return False
|
||||
```
|
||||
|
||||
## CONTEXT.md Standard Format
|
||||
|
||||
**Every project CONTEXT.md must have these sections:**
|
||||
|
||||
### Required Sections
|
||||
1. **Quick Start - Infrastructure Overview** (table format)
|
||||
2. **Current State (READ THIS FIRST)** - Recent work, versions deployed
|
||||
3. **Anti-Patterns (DON'T DO THIS)** - Common mistakes to avoid
|
||||
4. **Where to Find Things** - File structure, key paths
|
||||
5. **Common Operations** - Copy-paste commands for frequent tasks
|
||||
6. **Recent Session Logs** - Links to latest work
|
||||
|
||||
### Optional Sections
|
||||
- Key Technical Decisions (ADRs)
|
||||
- Troubleshooting (FAQ format)
|
||||
- Roadmap
|
||||
- Quick Reference (API endpoints, log formats, etc.)
|
||||
|
||||
## Implementation in .claude/CLAUDE.md
|
||||
|
||||
**Add this section to CLAUDE.md:**
|
||||
|
||||
```markdown
|
||||
## Automatic Context Loading (MANDATORY)
|
||||
|
||||
**BEFORE responding to ANY user message, check these triggers:**
|
||||
|
||||
### Trigger 1: Project Keywords
|
||||
If user mentions GuruRMM, Dataforth, tunnel, VASLOG, AD2, etc:
|
||||
1. Identify project from keyword
|
||||
2. Read [project]/CONTEXT.md ENTIRELY
|
||||
3. Note current state, infrastructure, anti-patterns
|
||||
4. THEN respond with full context
|
||||
|
||||
### Trigger 2: Continuation Words
|
||||
If user says "continue", "let's work on", "back to", "resume":
|
||||
1. Check for project in message
|
||||
2. Read CONTEXT.md if found
|
||||
3. Check recent session logs mentioned in CONTEXT.md
|
||||
4. THEN proceed with work
|
||||
|
||||
### Trigger 3: Infrastructure Questions
|
||||
If user asks about servers, databases, credentials, deployment:
|
||||
1. Check root CONTEXT.md for project list
|
||||
2. Read relevant project CONTEXT.md
|
||||
3. Answer from CONTEXT.md (don't ask user)
|
||||
|
||||
### Trigger 4: Uncertainty
|
||||
If you're <95% certain about infrastructure, recent work, or next steps:
|
||||
1. Search for CONTEXT.md in current working directory
|
||||
2. Search for CONTEXT.md in projects/*/
|
||||
3. Read any found before asking user
|
||||
|
||||
**ANTI-PATTERN EXAMPLE:**
|
||||
User: "Look at the Dataforth DFWDS folders"
|
||||
You: "I don't recall what we've done with Dataforth" # WRONG
|
||||
|
||||
**CORRECT PATTERN:**
|
||||
User: "Look at the Dataforth DFWDS folders"
|
||||
You: [Detects "Dataforth" → reads projects/dataforth-dos/CONTEXT.md]
|
||||
"I see from CONTEXT.md that recent work (2026-04-12) extended the SCMVAS/SCMHVAS
|
||||
pipeline. DFWDS folders are at C:\Shares\testdatadb\ on AD2 (192.168.0.6)..."
|
||||
```
|
||||
|
||||
## Hook Integration
|
||||
|
||||
**Create .claude/hooks/pre-response:**
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Pre-response hook: Check for CONTEXT.md
|
||||
|
||||
USER_MSG="$1"
|
||||
|
||||
# Check for project keywords
|
||||
if echo "$USER_MSG" | grep -qi "GuruRMM\|tunnel\|rmm-api"; then
|
||||
echo "[HINT] Load projects/msp-tools/guru-rmm/CONTEXT.md"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if echo "$USER_MSG" | grep -qi "Dataforth\|DFWDS\|testdatadb\|AD2\|VASLOG"; then
|
||||
echo "[HINT] Load projects/dataforth-dos/CONTEXT.md"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check for continuation words
|
||||
if echo "$USER_MSG" | grep -qi "continue\|back to\|work on\|resume"; then
|
||||
echo "[HINT] Check for active project CONTEXT.md"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check for infrastructure questions
|
||||
if echo "$USER_MSG" | grep -qi "server\|database\|deploy\|credentials"; then
|
||||
echo "[HINT] Check CONTEXT.md in current directory or projects/*/"
|
||||
exit 0
|
||||
fi
|
||||
```
|
||||
|
||||
## Session Start Protocol
|
||||
|
||||
**At the start of EVERY new session/conversation:**
|
||||
|
||||
1. Check current working directory for CONTEXT.md
|
||||
2. If found, read it BEFORE asking user anything
|
||||
3. If not found, check for projects/*/CONTEXT.md
|
||||
4. List available projects from found CONTEXT.md files
|
||||
5. Wait for user to specify what to work on
|
||||
6. When they do, load that project's CONTEXT.md
|
||||
|
||||
**Example session start:**
|
||||
|
||||
```
|
||||
[Claude starts, reads CONTEXT.md in /Users/azcomputerguru/ClaudeTools/]
|
||||
|
||||
Claude: "I've loaded context for ClaudeTools project. Available subprojects:
|
||||
- GuruRMM (tunnel Phase 2 pending)
|
||||
- Dataforth DOS (SCMVAS/SCMHVAS pipeline deployed)
|
||||
|
||||
What would you like to work on?"
|
||||
|
||||
User: "GuruRMM tunnel"
|
||||
|
||||
[Claude reads projects/msp-tools/guru-rmm/CONTEXT.md]
|
||||
|
||||
Claude: "Loaded GuruRMM context:
|
||||
- Server: 172.16.3.30:3001
|
||||
- Phase 1 complete (v0.6.0)
|
||||
- 2/6 agents online
|
||||
- Next: Channel implementation
|
||||
|
||||
Ready to proceed with Phase 2."
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
### For User
|
||||
- ✅ Never repeat context ("What's the server IP?")
|
||||
- ✅ Claude starts work immediately with full context
|
||||
- ✅ No "let me check session logs" delays
|
||||
- ✅ Consistent infrastructure knowledge
|
||||
|
||||
### For Claude
|
||||
- ✅ Clear decision tree for when to load context
|
||||
- ✅ Structured hints (CONTEXT.md) are fast to read
|
||||
- ✅ Anti-patterns prevent repeated mistakes
|
||||
- ✅ Recent session log pointers for deep-dive
|
||||
|
||||
### For System
|
||||
- ✅ Scales to N projects (just add CONTEXT.md)
|
||||
- ✅ Works across machines (Git-synced)
|
||||
- ✅ Low overhead (only read when triggered)
|
||||
- ✅ Degrades gracefully (if CONTEXT.md missing, ask user)
|
||||
|
||||
## Migration Path
|
||||
|
||||
### Phase 1: Create CONTEXT.md files (DONE ✅)
|
||||
- [x] Root: CONTEXT.md
|
||||
- [x] GuruRMM: projects/msp-tools/guru-rmm/CONTEXT.md
|
||||
- [x] Dataforth: projects/dataforth-dos/CONTEXT.md
|
||||
|
||||
### Phase 2: Update CLAUDE.md with auto-load rules
|
||||
```bash
|
||||
# Add "Automatic Context Loading" section to .claude/CLAUDE.md
|
||||
# Include triggers, anti-patterns, examples
|
||||
```
|
||||
|
||||
### Phase 3: Train via /refresh-directives
|
||||
```bash
|
||||
# Every time Claude starts session:
|
||||
# 1. Read CLAUDE.md (includes auto-load rules)
|
||||
# 2. Apply triggers to user's first message
|
||||
# 3. Load relevant CONTEXT.md before responding
|
||||
```
|
||||
|
||||
### Phase 4: Create hook (optional)
|
||||
```bash
|
||||
# .claude/hooks/pre-response
|
||||
# Automated hint detection, prints to stderr
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
**Test cases to verify system works:**
|
||||
|
||||
| User Message | Expected Behavior | Pass/Fail |
|
||||
|--------------|-------------------|-----------|
|
||||
| "Look at Dataforth DFWDS" | Reads dataforth-dos/CONTEXT.md, knows AD2 IP | ✅ |
|
||||
| "Continue GuruRMM tunnel" | Reads guru-rmm/CONTEXT.md, knows Phase 1 done | ✅ |
|
||||
| "What's the database password?" | Checks CONTEXT.md, finds vault command | ✅ |
|
||||
| "Deploy to AD2" | Reads dataforth/CONTEXT.md, knows deploy script | ✅ |
|
||||
| "Check agent status" | Reads guru-rmm/CONTEXT.md, knows API endpoint | ✅ |
|
||||
|
||||
## Example: Before and After
|
||||
|
||||
### BEFORE (Current Problem)
|
||||
```
|
||||
User: "Look at the Dataforth DFWDS folders"
|
||||
|
||||
Claude: "I'm not sure what work we've done on Dataforth. Let me search session logs..."
|
||||
[Searches for 5 minutes]
|
||||
[Finds multiple sessions]
|
||||
"Can you tell me which server has the DFWDS folders?"
|
||||
|
||||
User: [Frustrated] "It's AD2 at 192.168.0.6, we just worked on this!"
|
||||
```
|
||||
|
||||
### AFTER (With Auto-Context)
|
||||
```
|
||||
User: "Look at the Dataforth DFWDS folders"
|
||||
|
||||
Claude: [Detects "Dataforth" → reads projects/dataforth-dos/CONTEXT.md]
|
||||
"I see from CONTEXT.md that DFWDS folders are at C:\Shares\testdatadb\
|
||||
on AD2 (192.168.0.6). Recent work (2026-04-12) extended the SCMVAS/SCMHVAS
|
||||
pipeline. The testdatadb service is running on port 3000.
|
||||
|
||||
What would you like me to check in those folders?"
|
||||
|
||||
User: [Happy] "Perfect, let's verify the Engineering-Tested files imported correctly"
|
||||
```
|
||||
|
||||
## Rollout Plan
|
||||
|
||||
**Immediate (2026-04-14):**
|
||||
- [x] Create CONTEXT.md for 3 main projects
|
||||
- [ ] Update CLAUDE.md with auto-load rules
|
||||
- [ ] Test with /refresh-directives
|
||||
|
||||
**Short-term (1 week):**
|
||||
- [ ] Add CONTEXT.md for remaining projects
|
||||
- [ ] Create pre-response hook
|
||||
- [ ] Document in README
|
||||
|
||||
**Long-term (ongoing):**
|
||||
- [ ] Update CONTEXT.md after major sessions
|
||||
- [ ] Add new projects' CONTEXT.md as they start
|
||||
- [ ] Refine triggers based on user feedback
|
||||
|
||||
---
|
||||
|
||||
**Implementation Status:** Phase 1 Complete (CONTEXT.md files created)
|
||||
**Next Step:** Update .claude/CLAUDE.md with automatic loading rules
|
||||
**Goal:** Claude is >95% certain before responding, never asks for context that's in CONTEXT.md
|
||||
@@ -1,5 +1,68 @@
|
||||
# ClaudeTools Project Context
|
||||
|
||||
## Multi-User Environment (CHECK FIRST)
|
||||
|
||||
This repo is shared across multiple team members. **At every session start, BEFORE doing anything else:**
|
||||
|
||||
1. **Read `.claude/identity.json`** (local, gitignored). If it exists, greet the user by name and proceed.
|
||||
2. **If identity.json does NOT exist** (first sync on a new machine):
|
||||
- Read `.claude/users.json` for the known user list
|
||||
- Ask: "This looks like a new machine. Are you **Mike Swanson** or **Howard Enos**? (Or someone new?)"
|
||||
- Based on their answer, create `.claude/identity.json`:
|
||||
```json
|
||||
{
|
||||
"user": "mike",
|
||||
"full_name": "Mike Swanson",
|
||||
"email": "mike@azcomputerguru.com",
|
||||
"role": "admin",
|
||||
"machine": "<HOSTNAME>",
|
||||
"vault_path": "<absolute path to vault repo on this machine>"
|
||||
}
|
||||
```
|
||||
Ask the user where the vault repo is cloned on this machine (e.g., `D:/vault`, `~/vault`, `/Users/howard/vault`).
|
||||
- Set local git config: `git config user.name "<full_name>"` and `git config user.email "<email>"`
|
||||
- Set git remote (read `gitea_username` from users.json): `git remote set-url origin https://<gitea_username>@git.azcomputerguru.com/azcomputerguru/claudetools.git`
|
||||
- Add hostname to user's `known_machines` in users.json and commit.
|
||||
- **Show the user `.claude/ONBOARDING.md`** — present section by section, explain the WHY, answer questions.
|
||||
3. **If hostname doesn't match any known machine** for the identified user, update their `known_machines` in users.json.
|
||||
|
||||
### Session Log Attribution
|
||||
|
||||
Every session log MUST include a `## User` section:
|
||||
```markdown
|
||||
## User
|
||||
- **User:** Mike Swanson (mike)
|
||||
- **Machine:** DESKTOP-0O8A1RL
|
||||
- **Role:** admin
|
||||
```
|
||||
|
||||
Commits use local git config (user.name / user.email). Gitea push account is shared (azcomputerguru) but commit authorship tracks the actual person.
|
||||
|
||||
### Current Team
|
||||
|
||||
| User | Role | Notes |
|
||||
|---|---|---|
|
||||
| **Mike Swanson** (mike) | admin | Owner, President of Arizona Computer Guru LLC |
|
||||
| **Howard Enos** (howard) | tech | Employee, technician. Full trust — same access as admin. |
|
||||
|
||||
---
|
||||
|
||||
## Work Mode
|
||||
|
||||
Auto-detect on every user message (first match wins):
|
||||
|
||||
| Mode | Triggers | Posture |
|
||||
|------|----------|---------|
|
||||
| **remediation** | "remediation tool", "365", "breach", "tenant sweep", M365 keywords | Graph API focus, compliance language, full audit trail |
|
||||
| **client** | client name, `clients/` work, "for \<client\>" | Careful with data, session logs in `clients/`, name the client |
|
||||
| **infra** | server names/IPs, SSH, firewall, DNS, deploy, service restart | Confirm before destructive ops, backup-first |
|
||||
| **dev** | code, build, Rust/cargo, npm, GuruRMM dev, `projects/` work | Delegate freely, less confirmation friction |
|
||||
| **general** | default | Lightweight |
|
||||
|
||||
On mode change: announce `[MODE -> infra]`, tell user to run `/color <color>`. Full details: `.claude/commands/mode.md`
|
||||
|
||||
---
|
||||
|
||||
## Identity: You Are a Coordinator
|
||||
|
||||
You are NOT an executor. You coordinate specialized agents and preserve your context window.
|
||||
@@ -21,27 +84,64 @@ You are NOT an executor. You coordinate specialized agents and preserve your con
|
||||
**Do yourself:** Simple responses, reading 1-2 files, presenting results, planning, decisions.
|
||||
**Rule:** >500 tokens of work = delegate. Code or database = ALWAYS delegate.
|
||||
|
||||
**DO NOT** query databases directly (no SSH/mysql/curl to API). **DO NOT** write production code. **DO NOT** run tests. **DO NOT** commit/push. Use the appropriate agent.
|
||||
**DO NOT** query databases directly. **DO NOT** write production code. **DO NOT** run tests. **DO NOT** commit/push.
|
||||
|
||||
### Coordination Flow
|
||||
### Model Routing (Complexity-Based)
|
||||
|
||||
```
|
||||
User request -> Main Claude (coordinator) -> Launches agent(s) -> Agent returns summary -> Main Claude presents to user
|
||||
```
|
||||
| Tier | Model | When |
|
||||
|------|-------|------|
|
||||
| 0 | **Ollama** (local) | Low-stakes: summarize, classify, extract, draft — no code changes, output reviewed before use |
|
||||
| 1 | `haiku` | Ollama unavailable, or task needs agent tool use / file access |
|
||||
| 2 | (inherit) | Standard code, DB, tests, git — most work |
|
||||
| 3 | `opus` | Architecture, security, ambiguous failures, production risk |
|
||||
|
||||
- Independent operations run in parallel
|
||||
- Skills (Skill tool) enhance/validate. Agents (Agent tool) execute/operate.
|
||||
**Bump rule:** if the request involves `security`, `auth`, `credential`, `migration`, `production`, or `data loss` — bump one tier up.
|
||||
|
||||
Pass `model: "haiku"` or `model: "opus"` explicitly. Omit for Tier 2. Tier 0 is a direct Bash call — see `.claude/OLLAMA.md`.
|
||||
|
||||
---
|
||||
|
||||
## Automatic Context Loading (CRITICAL)
|
||||
|
||||
**BEFORE responding to the first message or when switching projects, AUTOMATICALLY load context:**
|
||||
|
||||
### Trigger 1: Project Keywords Detected
|
||||
If user mentions **GuruRMM**, **Dataforth**, **tunnel**, **VASLOG**, **AD2**, **testdatadb**, etc:
|
||||
1. Read the matching project CONTEXT.md:
|
||||
- GuruRMM keywords → `projects/msp-tools/guru-rmm/CONTEXT.md`
|
||||
- Dataforth keywords → `projects/dataforth-dos/CONTEXT.md`
|
||||
- General → `CONTEXT.md` (root)
|
||||
2. If `PROJECT_STATE.md` exists alongside CONTEXT.md, read it **and** `.claude/PROJECT_STATE_PROTOCOL.md`.
|
||||
3. THEN respond with full context.
|
||||
|
||||
### Trigger 2: Continuation/Resume Words
|
||||
If user says "continue", "let's work on", "back to", "resume", "finish":
|
||||
1. Detect project from message, read project CONTEXT.md.
|
||||
2. If PROJECT_STATE.md exists, read it and `.claude/PROJECT_STATE_PROTOCOL.md`.
|
||||
3. Check "Current State" and "Recent Session Logs" sections, then proceed.
|
||||
|
||||
### Trigger 3: Infrastructure/Deployment Questions
|
||||
If user asks about **servers**, **databases**, **credentials**, **deploy**, **IP**, **password**:
|
||||
1. Check current directory for CONTEXT.md, then `projects/*/CONTEXT.md`.
|
||||
2. Answer from CONTEXT.md — never ask for info that's already there.
|
||||
|
||||
### Trigger 4: Uncertainty >5%
|
||||
If you're <95% certain about infrastructure, recent work, or next steps: read CONTEXT.md before asking the user.
|
||||
|
||||
### Anti-Pattern
|
||||
|
||||
Never ask "What did we do last time?" or "What's the server IP?" — read the CONTEXT.md first. If it's not there, then ask.
|
||||
|
||||
---
|
||||
|
||||
## Projects
|
||||
|
||||
**ClaudeTools** -- MSP Work Tracking System (Production-Ready)
|
||||
**ClaudeTools** — MSP Work Tracking System (Production-Ready)
|
||||
- Database: MariaDB 10.6.22 @ 172.16.3.30:3306 | API: http://172.16.3.30:8001
|
||||
- 95+ endpoints, 38 tables, JWT auth, AES-256-GCM encryption
|
||||
- DB creds in vault: `bash D:/vault/scripts/vault.sh get-field projects/claudetools/database.sops.yaml credentials.password`
|
||||
- DB creds: `bash D:/vault/scripts/vault.sh get-field projects/claudetools/database.sops.yaml credentials.password`
|
||||
|
||||
**GuruRMM** -- Remote Monitoring & Management (Active Development)
|
||||
**GuruRMM** — Remote Monitoring & Management (Active Development)
|
||||
- Server: Rust/Axum @ 172.16.3.30:3001 | Dashboard: https://rmm.azcomputerguru.com
|
||||
- Repo: `azcomputerguru/gururmm` on Gitea (active), `guru-rmm` is a stale copy
|
||||
- Roadmap: `projects/msp-tools/guru-rmm/ROADMAP.md`
|
||||
@@ -50,51 +150,66 @@ User request -> Main Claude (coordinator) -> Launches agent(s) -> Agent returns
|
||||
|
||||
## Key Rules
|
||||
|
||||
- **NO EMOJIS** - Use ASCII markers: `[OK]`, `[ERROR]`, `[WARNING]`, `[SUCCESS]`, `[INFO]`
|
||||
- **No hardcoded credentials** - Use SOPS vault (`vault get-field <path> <field>`) or 1Password as fallback
|
||||
- **SSH:** Use system OpenSSH (on Windows: `C:\Windows\System32\OpenSSH\ssh.exe`, never Git for Windows SSH)
|
||||
- **NO EMOJIS** — Use ASCII markers: `[OK]`, `[ERROR]`, `[WARNING]`, `[SUCCESS]`, `[INFO]`
|
||||
- **No hardcoded credentials** — Use SOPS vault (`vault get-field <path> <field>`) or 1Password as fallback
|
||||
- **SSH:** Use system OpenSSH (`C:\Windows\System32\OpenSSH\ssh.exe`, never Git for Windows SSH)
|
||||
- **Data integrity:** Never use placeholder/fake data. Check SOPS vault, credentials.md, or ask user.
|
||||
- **Coding standards:** `.claude/CODING_GUIDELINES.md` (agents read on-demand, not every session)
|
||||
- **Coding standards:** `.claude/CODING_GUIDELINES.md` (agents read on-demand)
|
||||
|
||||
---
|
||||
|
||||
## Automatic Behaviors
|
||||
|
||||
- **Frontend Design:** Auto-invoke `/frontend-design` skill after ANY UI change (HTML/CSS/JSX/styling)
|
||||
- **Sequential Thinking:** Use for genuine complexity - rejection loops, 3+ critical issues, architectural decisions, multi-step debugging
|
||||
- **Task Management:** Complex work (>3 steps) -> TaskCreate. Persist to `.claude/active-tasks.json`.
|
||||
- **Sequential Thinking:** Use for genuine complexity — rejection loops, 3+ critical issues, architectural decisions
|
||||
- **Task Management:** Complex work (>3 steps) → TaskCreate. Persist to `.claude/active-tasks.json`.
|
||||
|
||||
### Cross-User Messages (MANDATORY)
|
||||
|
||||
After every `/sync` — and whenever reading any session log from another user — scan for sections named `## Note for <user>` or `## Message for <user>`. If found:
|
||||
|
||||
1. Display the full note content **prominently at the top of the response**, before sync status or anything else, formatted as:
|
||||
```
|
||||
============================================================
|
||||
MESSAGE FROM <AUTHOR> (<date>)
|
||||
============================================================
|
||||
<full note content>
|
||||
============================================================
|
||||
```
|
||||
2. Explicitly address each action item or question in the note before moving on.
|
||||
3. Do not bury these in the sync summary or skip them because other work is in progress.
|
||||
|
||||
This applies to ALL session logs pulled during sync, not just the most recent one.
|
||||
|
||||
---
|
||||
|
||||
## Context Recovery
|
||||
|
||||
When user references previous work, use `/context` command. Never ask user for info in:
|
||||
- `credentials.md` - Infrastructure reference (being migrated to SOPS vault at D:\vault)
|
||||
- `session-logs/` - Daily work logs (also in `projects/*/session-logs/` and `clients/*/session-logs/`)
|
||||
- `SESSION_STATE.md` - Project history
|
||||
When user references previous work, use `/context` command. Never ask for info in:
|
||||
- `credentials.md` — Infrastructure reference (being migrated to SOPS vault at D:\vault)
|
||||
- `session-logs/` — Daily work logs (also in `projects/*/session-logs/` and `clients/*/session-logs/`)
|
||||
- `SESSION_STATE.md` — Project history
|
||||
|
||||
### Credential Access (SOPS Vault - Primary)
|
||||
### Credential Access (SOPS Vault)
|
||||
|
||||
Credentials are stored in SOPS+age encrypted YAML files in a dedicated Gitea repo.
|
||||
Use the ClaudeTools vault wrapper — never hardcode the vault path:
|
||||
|
||||
**Vault repo:** `D:\vault` (git.azcomputerguru.com/azcomputerguru/vault, private)
|
||||
**Structure:** infrastructure/, clients/, services/, projects/, msp-tools/
|
||||
|
||||
**To read credentials:**
|
||||
```bash
|
||||
bash D:/vault/scripts/vault.sh search "keyword" # Search (no decryption needed)
|
||||
bash D:/vault/scripts/vault.sh get-field <path> <field> # Get specific field
|
||||
bash D:/vault/scripts/vault.sh get <path> # Decrypt full entry
|
||||
bash D:/vault/scripts/vault.sh list # List all entries
|
||||
# CLAUDETOOLS_ROOT is the repo root (D:\claudetools on Windows, ~/claudetools on Mac/Linux)
|
||||
VAULT="$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh"
|
||||
|
||||
bash "$VAULT" search "keyword" # Search without decrypting
|
||||
bash "$VAULT" get-field <path> <field> # Get specific field
|
||||
bash "$VAULT" get <path> # Decrypt full entry
|
||||
bash "$VAULT" list # List all entries
|
||||
```
|
||||
|
||||
**Encryption:** AES-256 via age. Metadata stays plaintext for searchability.
|
||||
The wrapper reads `vault_path` from `.claude/identity.json` (per-machine, gitignored).
|
||||
Each machine sets its own vault path there — no hardcoded paths in any shared file.
|
||||
|
||||
**age key location:** `%APPDATA%\sops\age\keys.txt` (Windows) / `~/.config/sops/age/keys.txt` (Linux/Mac)
|
||||
Vault structure: `infrastructure/`, `clients/`, `services/`, `projects/`, `msp-tools/`
|
||||
|
||||
### 1Password (Fallback)
|
||||
|
||||
Service account token in vault: `infrastructure/1password-service-account.sops.yaml`
|
||||
**1Password fallback:** service account token in `infrastructure/1password-service-account.sops.yaml`
|
||||
|
||||
---
|
||||
|
||||
@@ -103,42 +218,34 @@ Service account token in vault: `infrastructure/1password-service-account.sops.y
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `/checkpoint` | Dual checkpoint: git commit + database context |
|
||||
| `/save` | Comprehensive session log (credentials, decisions, changes) |
|
||||
| `/save` | Comprehensive session log |
|
||||
| `/context` | Search session logs, credentials.md, and 1Password |
|
||||
| `/1password` | 1Password secrets management integration |
|
||||
| `/1password` | 1Password secrets management |
|
||||
| `/sync` | Sync config from Gitea repository |
|
||||
| `/create-spec` | Create app specification for AutoCoder |
|
||||
| `/frontend-design` | Modern frontend design patterns (auto-invoke after UI changes) |
|
||||
| `/frontend-design` | Modern frontend design (auto-invoke after UI changes) |
|
||||
| `/remediation-tool` | M365 breach checks, tenant sweeps, gated remediation |
|
||||
|
||||
---
|
||||
|
||||
## File Placement (Quick Rules)
|
||||
## File Placement
|
||||
|
||||
- **Dataforth DOS work** -> `projects/dataforth-dos/`
|
||||
- **ClaudeTools API code** -> `api/`, `migrations/` (existing structure)
|
||||
- **GuruRMM work** -> `projects/msp-tools/guru-rmm/`
|
||||
- **Client work** -> `clients/[client-name]/`
|
||||
- **Session logs** -> project or client `session-logs/` subfolder; general -> root `session-logs/`
|
||||
- **Full guide:** `.claude/FILE_PLACEMENT_GUIDE.md` (read when saving files, not every session)
|
||||
- **Dataforth DOS work** → `projects/dataforth-dos/`
|
||||
- **ClaudeTools API code** → `api/`, `migrations/`
|
||||
- **GuruRMM work** → `projects/msp-tools/guru-rmm/`
|
||||
- **Client work** → `clients/[client-name]/`
|
||||
- **Session logs** → project or client `session-logs/` subfolder; general → root `session-logs/`
|
||||
- **Full guide:** `.claude/FILE_PLACEMENT_GUIDE.md`
|
||||
|
||||
---
|
||||
|
||||
## Local AI (Ollama)
|
||||
|
||||
Ollama runs locally with GPU acceleration for tasks that don't need Claude-level reasoning.
|
||||
|
||||
| Model | Size | Use For |
|
||||
|-------|------|---------|
|
||||
| `qwen3:14b` | 9.3 GB | Summarization, classification, data extraction, drafting |
|
||||
| `codestral:22b` | 12 GB | Code generation, refactoring suggestions, docstrings |
|
||||
| `nomic-embed-text` | 274 MB | Embeddings only (used by GrepAI) |
|
||||
|
||||
```bash
|
||||
# Simple prompt
|
||||
curl -s http://localhost:11434/api/generate -d '{"model":"qwen3:14b","prompt":"...","stream":false}' | jq -r '.response'
|
||||
```
|
||||
|
||||
**Review policy:** Always review Critical/High impact Ollama outputs (auth, security, migrations, production). Trust Low impact (classification, formatting). Flag uncertainty to user.
|
||||
Tier 0 — use before spawning Haiku agents for low-stakes tasks (free, fast, private):
|
||||
- **DESKTOP-0O8A1RL:** `http://localhost:11434`
|
||||
- **Other machines:** `http://100.92.127.64:11434` (Tailscale required)
|
||||
- **Models:** `qwen3:14b` (summarize/classify/draft), `codestral:22b` (code suggestions — always review)
|
||||
- **Full reference:** `.claude/OLLAMA.md` (connection examples, model selection, review policy)
|
||||
|
||||
### GrepAI (Semantic Code Search)
|
||||
|
||||
@@ -151,7 +258,7 @@ Use for intent-based search ("how does auth work"), exploring unfamiliar code, c
|
||||
|
||||
## Memory (Shared Across Machines)
|
||||
|
||||
Stored in-repo at `.claude/memory/` -- syncs via Gitea to all workstations.
|
||||
Stored in-repo at `.claude/memory/` — syncs via Gitea to all workstations.
|
||||
Index: `.claude/memory/MEMORY.md`
|
||||
|
||||
**IMPORTANT:** Always write to `.claude/memory/` (repo-relative), NOT `~/.claude/projects/*/memory/`.
|
||||
@@ -164,7 +271,9 @@ Index: `.claude/memory/MEMORY.md`
|
||||
- **Agent definitions:** `.claude/agents/*.md`
|
||||
- **MCP servers:** `MCP_SERVERS.md`
|
||||
- **Coding standards:** `.claude/CODING_GUIDELINES.md`
|
||||
- **Ollama connection + examples:** `.claude/OLLAMA.md`
|
||||
- **PROJECT_STATE locking protocol:** `.claude/PROJECT_STATE_PROTOCOL.md`
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2026-04-02
|
||||
**Last Updated:** 2026-04-20
|
||||
|
||||
74
.claude/COMPLEXITY_ROUTING.md
Normal file
74
.claude/COMPLEXITY_ROUTING.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Complexity-Based Model Routing
|
||||
|
||||
When spawning an agent, pick a tier based on the request signals below, then pass `model` accordingly.
|
||||
|
||||
---
|
||||
|
||||
## Tier 1 — Haiku (fast/cheap)
|
||||
|
||||
**Signals:** single lookup, no code changes, classification, formatting, summarization, status check, documentation
|
||||
|
||||
**Examples:**
|
||||
- "What's the status of X?"
|
||||
- Summarize or format a session log
|
||||
- Search/grep for a value
|
||||
- Convert or extract data
|
||||
- Write/update a markdown doc
|
||||
|
||||
**Agents that default here:** documentation-squire, explore (quick searches), photo
|
||||
|
||||
**Agent call:** `model: "haiku"`
|
||||
|
||||
---
|
||||
|
||||
## Tier 2 — Sonnet (default, inherit)
|
||||
|
||||
**Signals:** standard code generation, routine DB queries, test execution, API work, multi-file reads, git operations
|
||||
|
||||
**Examples:**
|
||||
- Add or modify an endpoint
|
||||
- Run tests and report results
|
||||
- Write a DB migration
|
||||
- Fetch credentials, configure a service
|
||||
- Commit and push changes
|
||||
|
||||
**Agents that default here:** coding, database, testing, gitea, general-purpose, deep-explore (standard search)
|
||||
|
||||
**Agent call:** omit `model` (inherits session model)
|
||||
|
||||
---
|
||||
|
||||
## Tier 3 — Opus (high-stakes reasoning)
|
||||
|
||||
**Signals:** architectural decision, security/auth, 3+ interacting systems, ambiguous root cause, production data risk, anything that fails badly if wrong
|
||||
|
||||
**Examples:**
|
||||
- Redesign an auth or data flow
|
||||
- Security or code review of a critical PR
|
||||
- Debug a multi-service race condition
|
||||
- Schema migration on production data
|
||||
- Evaluate competing architectural approaches
|
||||
|
||||
**Agents that default here:** code-review (when Sequential Thinking triggers), deep-explore (architecture questions)
|
||||
|
||||
**Agent call:** `model: "opus"`
|
||||
|
||||
---
|
||||
|
||||
## Bump Rule
|
||||
|
||||
If the request contains ANY of these keywords, bump one tier up regardless of other signals:
|
||||
|
||||
`security`, `auth`, `token`, `credential`, `migration`, `production`, `race condition`, `data loss`, `breach`, `encrypt`
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Tier | Model | Typical cost | Use when |
|
||||
|------|-------|-------------|----------|
|
||||
| 1 | `haiku` | ~10x cheaper | Lookup, format, summarize, doc |
|
||||
| 2 | (inherit) | baseline | Standard code, DB, tests |
|
||||
| 3 | `opus` | ~5x more expensive | Architecture, security, ambiguous failures |
|
||||
|
||||
Err toward Tier 2 when uncertain. Only use Opus when the reasoning stakes justify the cost.
|
||||
@@ -232,7 +232,7 @@ curl http://172.16.3.30:8001/health
|
||||
# Check total contexts
|
||||
curl -H "Authorization: Bearer $JWT" \
|
||||
http://172.16.3.30:8001/api/conversation-contexts | \
|
||||
python -c "import sys,json; print(f'Total: {json.load(sys.stdin)[\"total\"]}')"
|
||||
jq -r '.total'
|
||||
|
||||
# Try different search term
|
||||
# Instead of: search_term=dataforth%20DOS
|
||||
|
||||
197
.claude/MAC-vault-readiness-test.md
Normal file
197
.claude/MAC-vault-readiness-test.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Mac Vault Readiness Test Results
|
||||
|
||||
**Date:** 2026-04-21
|
||||
**Machine:** Mikes-MacBook-Air.local
|
||||
**Purpose:** Test vault access capability for remediation-tool
|
||||
|
||||
---
|
||||
|
||||
## Test Results Summary
|
||||
|
||||
**Status:** NOT READY - Multiple blockers present
|
||||
|
||||
### Dependencies Check
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| jq | ✓ INSTALLED | jq-1.7.1-apple |
|
||||
| SOPS | ✗ NOT INSTALLED | Required for decrypting .sops.yaml files |
|
||||
| age | ✗ NOT INSTALLED | Required for SOPS encryption/decryption |
|
||||
| age key | ✗ NOT CONFIGURED | ~/.config/sops/age/keys.txt missing |
|
||||
| vault repo | ✗ NOT CLONED | Git authentication blocked |
|
||||
| vault_path in identity.json | ✗ NOT SET | Would point to ~/vault once cloned |
|
||||
|
||||
### What Works
|
||||
|
||||
**[OK] Vault wrapper script exists and reports correct errors:**
|
||||
```bash
|
||||
bash .claude/scripts/vault.sh list
|
||||
→ [ERROR] vault_path not set in identity.json
|
||||
```
|
||||
|
||||
**[OK] get-token.sh bug fixes applied:**
|
||||
- Variable collision fixed (VAULT_PATH → VAULT_ROOT_ENV)
|
||||
- Directory traversal corrected (4 levels up instead of 3)
|
||||
|
||||
**[OK] Remediation-tool scripts are executable:**
|
||||
```bash
|
||||
ls -la .claude/skills/remediation-tool/scripts/*.sh
|
||||
→ All scripts have execute permissions
|
||||
```
|
||||
|
||||
### What's Blocked
|
||||
|
||||
**1. Vault Repository Clone**
|
||||
```bash
|
||||
git clone http://azcomputerguru@172.16.3.20:3000/azcomputerguru/vault.git ~/vault
|
||||
→ fatal: could not read Password: Device not configured
|
||||
```
|
||||
|
||||
Git cannot prompt for credentials in this terminal session.
|
||||
|
||||
**2. SOPS Installation**
|
||||
```bash
|
||||
sops --version
|
||||
→ command not found
|
||||
```
|
||||
|
||||
SOPS not installed via Homebrew or other package manager.
|
||||
|
||||
**3. age Installation**
|
||||
```bash
|
||||
age --version
|
||||
→ command not found
|
||||
```
|
||||
|
||||
age encryption tool not installed.
|
||||
|
||||
**4. age Key Configuration**
|
||||
```bash
|
||||
test -f ~/.config/sops/age/keys.txt
|
||||
→ File does not exist
|
||||
```
|
||||
|
||||
No SOPS age private key configured.
|
||||
|
||||
---
|
||||
|
||||
## What Would Be Required to Unblock
|
||||
|
||||
### Installation Steps (If Vault Access on Mac is Needed)
|
||||
|
||||
**1. Install Homebrew (if not already installed):**
|
||||
```bash
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
```
|
||||
|
||||
**2. Install SOPS:**
|
||||
```bash
|
||||
brew install sops
|
||||
```
|
||||
|
||||
**3. Install age:**
|
||||
```bash
|
||||
brew install age
|
||||
```
|
||||
|
||||
**4. Copy age private key from Windows:**
|
||||
|
||||
On Windows (DESKTOP-0O8A1RL):
|
||||
```bash
|
||||
cat C:\Users\<username>\.config\sops\age\keys.txt
|
||||
```
|
||||
|
||||
On Mac:
|
||||
```bash
|
||||
mkdir -p ~/.config/sops/age
|
||||
# Paste the private key content into:
|
||||
nano ~/.config/sops/age/keys.txt
|
||||
chmod 600 ~/.config/sops/age/keys.txt
|
||||
```
|
||||
|
||||
**5. Configure Git credential helper:**
|
||||
```bash
|
||||
git config --global credential.helper osxkeychain
|
||||
```
|
||||
|
||||
**6. Clone vault repository:**
|
||||
```bash
|
||||
git clone http://azcomputerguru@172.16.3.20:3000/azcomputerguru/vault.git ~/vault
|
||||
# Will prompt for password - enter Gitea password
|
||||
```
|
||||
|
||||
**7. Add vault_path to identity.json:**
|
||||
```bash
|
||||
# Edit .claude/identity.json and add:
|
||||
"vault_path": "/Users/azcomputerguru/vault"
|
||||
```
|
||||
|
||||
**8. Test token acquisition:**
|
||||
```bash
|
||||
cd .claude/skills/remediation-tool/scripts
|
||||
./get-token.sh grabblaw.com investigator
|
||||
```
|
||||
|
||||
Should return a JWT token if all configured correctly.
|
||||
|
||||
---
|
||||
|
||||
## Is This Worth Doing?
|
||||
|
||||
**Probably not, unless you need remediation-tool on Mac.**
|
||||
|
||||
**Why it's not urgent:**
|
||||
- Windows (DESKTOP-0O8A1RL) has working vault + remediation-tool ✓
|
||||
- Vault sync validated on Windows - all 5 tiers working ✓
|
||||
- Howard can be unblocked by pulling vault on ACG-Tech03L ✓
|
||||
- Mac is just for testing/portability
|
||||
|
||||
**Use cases for Mac vault:**
|
||||
- Running breach checks while away from Windows desktop
|
||||
- Testing remediation-tool portability across platforms
|
||||
- Validating vault sync from Mac perspective
|
||||
|
||||
**Alternatives:**
|
||||
- Use Windows for all remediation-tool work (current state)
|
||||
- SSH into Windows from Mac when needed
|
||||
- Remote desktop to Windows desktop
|
||||
|
||||
---
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Skip Mac vault setup for now.**
|
||||
|
||||
**Reasons:**
|
||||
1. Windows already validated vault sync works
|
||||
2. All 5 SOPS files confirmed present
|
||||
3. Token acquisition tested on all 5 tiers
|
||||
4. Howard can be notified to pull
|
||||
5. Mac setup requires 4 installations + credential management
|
||||
|
||||
**Only set up Mac vault if:**
|
||||
- You frequently work from Mac and need remediation-tool
|
||||
- You want to test cross-platform portability
|
||||
- Windows desktop is unavailable for extended periods
|
||||
|
||||
---
|
||||
|
||||
## Current Capability on Mac
|
||||
|
||||
**What works:**
|
||||
- Reading/editing remediation-tool scripts
|
||||
- Viewing tenant lists (references/tenants.md)
|
||||
- Resolving tenant IDs: `./resolve-tenant.sh <domain>`
|
||||
- All other ClaudeTools functionality
|
||||
|
||||
**What doesn't work:**
|
||||
- Token acquisition (no vault)
|
||||
- SOPS decryption (no vault + no SOPS)
|
||||
- Running breach checks (needs tokens)
|
||||
- Testing remediation-tool workflows (needs tokens)
|
||||
|
||||
---
|
||||
|
||||
**Status:** Documented and understood - Mac not currently set up for vault access
|
||||
**Action:** No action needed unless Mac remediation-tool access becomes necessary
|
||||
**Validated on:** Windows (DESKTOP-0O8A1RL) - all 5 tiers working
|
||||
109
.claude/MCP_SERVERS.md
Normal file
109
.claude/MCP_SERVERS.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# MCP Servers — Configuration Reference
|
||||
|
||||
MCP (Model Context Protocol) servers extend Claude Code with external tool
|
||||
capabilities. Each server runs as a child process and exposes tools that
|
||||
Claude can call.
|
||||
|
||||
**Config file:** `.mcp.json` in repo root (shared across machines via git).
|
||||
|
||||
---
|
||||
|
||||
## Active Servers
|
||||
|
||||
### TickTick
|
||||
|
||||
Task management integration for TickTick (todo/project tracking app).
|
||||
|
||||
**Tools provided:**
|
||||
- `ticktick_create_task`, `ticktick_update_task`, `ticktick_complete_task`, `ticktick_delete_task`
|
||||
- `ticktick_create_project`, `ticktick_update_project`, `ticktick_delete_project`
|
||||
- `ticktick_list_projects`, `ticktick_get_project`
|
||||
|
||||
**Auth:** OAuth token stored in vault at `services/ticktick.sops.yaml`. Token file
|
||||
auto-generated by `mcp-servers/ticktick/ticktick_auth.py` on first use.
|
||||
|
||||
**Config in `.mcp.json`:**
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"ticktick": {
|
||||
"command": "python",
|
||||
"args": ["D:\\claudetools\\mcp-servers\\ticktick\\ticktick_mcp.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Claude-in-Chrome (browser automation)
|
||||
|
||||
Installed as a Chrome browser extension. Provides browser automation tools
|
||||
for web interaction, form filling, page reading, screenshots, GIF recording.
|
||||
|
||||
**Not configured in `.mcp.json`** — runs as a Chrome extension that connects
|
||||
automatically when the Claude Code extension is active and Chrome is open.
|
||||
|
||||
**Tools provided:** `tabs_context_mcp`, `tabs_create_mcp`, `navigate`, `computer`
|
||||
(click/type/screenshot), `read_page`, `find`, `form_input`, `javascript_tool`,
|
||||
`get_page_text`, `read_console_messages`, `gif_creator`, etc.
|
||||
|
||||
**Requires:** Chrome browser with the Claude-in-Chrome extension installed.
|
||||
|
||||
---
|
||||
|
||||
## Available but Not Wired
|
||||
|
||||
These server directories exist but aren't in `.mcp.json`. Add them when needed.
|
||||
|
||||
### GrepAI MCP Server
|
||||
|
||||
Semantic code search over the indexed codebase. Alternative to using the
|
||||
`grepai search` CLI directly.
|
||||
|
||||
**To activate:** Add to `.mcp.json`:
|
||||
```json
|
||||
{
|
||||
"grepai": {
|
||||
"command": "D:\\claudetools\\grepai.exe",
|
||||
"args": ["mcp-serve"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Requires:** GrepAI initialized (`grepai init`) + Ollama running with
|
||||
`nomic-embed-text` model. Index builds automatically via `grepai watch`.
|
||||
|
||||
### Ollama Assistant
|
||||
|
||||
Local LLM integration for delegating simple tasks (summarization,
|
||||
classification, drafting) to locally-running models.
|
||||
|
||||
**Location:** `mcp-servers/ollama-assistant/`
|
||||
|
||||
**To activate:** Check the server's README for the exact `.mcp.json` entry.
|
||||
Requires Ollama running at `http://localhost:11434` with models pulled.
|
||||
|
||||
### Feature Management
|
||||
|
||||
Feature flag management server.
|
||||
|
||||
**Location:** `mcp-servers/feature-management/`
|
||||
|
||||
**Status:** Exists but purpose unclear. Check directory for README.
|
||||
|
||||
---
|
||||
|
||||
## Adding a New MCP Server
|
||||
|
||||
1. Create directory: `mcp-servers/<name>/`
|
||||
2. Write the server script (Python or Node recommended)
|
||||
3. Add entry to `.mcp.json` with `command` and `args`
|
||||
4. Restart Claude Code to pick up the new server
|
||||
5. Document in this file
|
||||
|
||||
**Important:** `.mcp.json` is tracked in git. Changes sync to all machines.
|
||||
Machine-specific server paths should use absolute paths that work on all
|
||||
team workstations (or use relative paths from repo root).
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2026-04-16*
|
||||
88
.claude/OLLAMA.md
Normal file
88
.claude/OLLAMA.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Ollama — Local AI Reference
|
||||
|
||||
Ollama runs on Mike's workstation (DESKTOP-0O8A1RL) with GPU acceleration. Available to all team members via Tailscale.
|
||||
|
||||
## Models
|
||||
|
||||
| Model | Size | Use For |
|
||||
|-------|------|---------|
|
||||
| `qwen3:14b` | 9.3 GB | Summarization, classification, data extraction, drafting |
|
||||
| `codestral:22b` | 12 GB | Code generation, refactoring suggestions, docstrings |
|
||||
| `nomic-embed-text` | 274 MB | Embeddings only (used by GrepAI) |
|
||||
|
||||
## Endpoints
|
||||
|
||||
Auto-detect: any machine that has a local Ollama listening on `127.0.0.1:11434` uses local. Otherwise fall back to Mike's workstation over Tailscale.
|
||||
|
||||
```bash
|
||||
# Preferred universal resolver — works on any machine
|
||||
if curl -s -m 2 http://localhost:11434/api/tags >/dev/null 2>&1; then
|
||||
OLLAMA="http://localhost:11434"
|
||||
else
|
||||
OLLAMA="http://100.92.127.64:11434"
|
||||
fi
|
||||
```
|
||||
|
||||
Rationale:
|
||||
- **Mike's workstation (DESKTOP-0O8A1RL):** local matches, no change.
|
||||
- **HOWARD-HOME:** also has a local Ollama with the canonical model set (confirmed 2026-04-22). Uses local — faster, zero Tailscale hop, no load on Mike's GPU.
|
||||
- **Other team machines:** no local Ollama → falls back to Mike's over Tailscale.
|
||||
- **Mike's machine offline:** graceful degradation — local users continue working; non-local users get a clean timeout.
|
||||
|
||||
Manual override (for testing or explicit preference): set `OLLAMA=http://100.92.127.64:11434` before the call.
|
||||
|
||||
Check reachability:
|
||||
```bash
|
||||
curl -s $OLLAMA/api/tags | jq -r '.models[].name'
|
||||
```
|
||||
|
||||
If neither endpoint responds: verify Tailscale (`tailscale status`) and whether your local Ollama service is running.
|
||||
|
||||
## Access Control
|
||||
|
||||
- Port 11434 allowed ONLY from Tailscale subnet (100.0.0.0/8)
|
||||
- NOT exposed to LAN, VPN, or internet
|
||||
- Binding: `OLLAMA_HOST=0.0.0.0:11434` (firewall restricts)
|
||||
|
||||
## Calling Ollama
|
||||
|
||||
Use the `/api/chat` endpoint with `think:false` for qwen3 models. The older `/api/generate` endpoint on qwen3 puts output into thinking tokens that don't appear in the `response` field — you'll get an empty response if you use `/api/generate`.
|
||||
|
||||
Preferred one-liner:
|
||||
```bash
|
||||
python -c "
|
||||
import urllib.request, json, sys, os
|
||||
OLLAMA = os.environ.get('OLLAMA') or ('http://localhost:11434' if __import__('urllib.request').request.urlopen(urllib.request.Request('http://localhost:11434/api/tags'),timeout=2) else 'http://100.92.127.64:11434')
|
||||
body = json.dumps({
|
||||
'model':'qwen3:14b',
|
||||
'messages':[{'role':'user','content': sys.argv[1]}],
|
||||
'stream':False,
|
||||
'think':False
|
||||
}).encode()
|
||||
res = json.loads(urllib.request.urlopen(urllib.request.Request(OLLAMA+'/api/chat', body), timeout=120).read())
|
||||
print(res['message']['content'])
|
||||
" "Your prompt here"
|
||||
```
|
||||
|
||||
Or set `$OLLAMA` once from bash (see auto-detect formula above) and reuse it across calls.
|
||||
|
||||
For code suggestions, swap `qwen3:14b` for `codestral:22b`. Codestral doesn't need `think:false`.
|
||||
|
||||
Cold-start is ~30-50s on first call per model per session. Warm calls are 1-5s.
|
||||
|
||||
## When to Use Which Model
|
||||
|
||||
| Task | Model |
|
||||
|------|-------|
|
||||
| Summarize logs, diffs, session notes | qwen3:14b |
|
||||
| Classify bug type, severity, category | qwen3:14b |
|
||||
| Extract structured data from text | qwen3:14b |
|
||||
| Draft commit message from diff | qwen3:14b |
|
||||
| Suggest refactor for a function | codestral:22b |
|
||||
| Docstring / comment generation | codestral:22b |
|
||||
|
||||
## Review Policy
|
||||
|
||||
- Low-stakes output (summary, classification, draft) — use directly
|
||||
- Code suggestions from codestral — always review before applying
|
||||
- Never use Ollama for: auth decisions, credential handling, production migrations, security review
|
||||
274
.claude/ONBOARDING.md
Normal file
274
.claude/ONBOARDING.md
Normal file
@@ -0,0 +1,274 @@
|
||||
# Welcome to ClaudeTools — Onboarding Guide
|
||||
|
||||
Hey! This guide explains how our Claude Code setup works, WHY it's built the way it is, and how to use it effectively for daily MSP work. Read this once, then use it as reference when something feels unfamiliar.
|
||||
|
||||
---
|
||||
|
||||
## What is this?
|
||||
|
||||
ClaudeTools is our shared workspace for **Claude Code** — the AI coding + automation assistant. It's a git repo that syncs across our workstations via Gitea (our self-hosted Git server). Everything Claude learns, every session log, every automation script, every project we build — it all lives here and stays in sync.
|
||||
|
||||
**Why a repo instead of just using Claude directly?**
|
||||
- Claude Code loses context between sessions. This repo IS the memory.
|
||||
- Session logs preserve what we did, what creds we used, what decisions we made.
|
||||
- CLAUDE.md tells Claude HOW to behave specifically for our org (not generic defaults).
|
||||
- Skills and commands give us reusable shortcuts for common MSP tasks.
|
||||
- The vault (separate repo) stores all credentials encrypted so Claude can access them without us typing passwords every session.
|
||||
|
||||
---
|
||||
|
||||
## First time setup
|
||||
|
||||
When you open Claude Code for the first time on a new machine, Claude will ask who you are. Just answer with your name. Claude then:
|
||||
|
||||
1. Creates a local identity file (so it knows who's at the keyboard)
|
||||
2. Sets your git name/email for commits
|
||||
3. Registers your machine in the shared users list
|
||||
|
||||
After that, every session log and git commit is attributed to you.
|
||||
|
||||
### GuruRMM repo — one-time setup per machine
|
||||
|
||||
The GuruRMM repo (`projects/msp-tools/guru-rmm/`) requires one extra step after cloning or first use. Run this from the repo root:
|
||||
|
||||
```bash
|
||||
bash scripts/install-hooks.sh
|
||||
```
|
||||
|
||||
This does three things permanently:
|
||||
- Points git at `scripts/hooks/` so pre-commit checks run automatically (and stay current as hooks evolve — no re-install after updates)
|
||||
- Sets `core.autocrlf=false` and `core.eol=lf` for this repo (prevents sqlx migration checksum drift from Windows CRLF line endings)
|
||||
- Sets `core.autocrlf=false` globally on this machine
|
||||
|
||||
**Why this matters:** sqlx verifies migration files by sha384 hash. A file committed with CRLF line endings hashes differently than the same file with LF — the server sees the mismatch and refuses to start. The `.gitattributes` file handles new commits automatically; this command configures the git client for existing checkouts.
|
||||
|
||||
---
|
||||
|
||||
## The slash commands (most important daily tools)
|
||||
|
||||
Type these in Claude Code's prompt. They're shortcuts for common operations.
|
||||
|
||||
| Command | What it does | When to use |
|
||||
|---------|-------------|-------------|
|
||||
| `/save` | Saves a comprehensive session log (what you did, creds used, decisions made) | **End of every significant work session.** This is how future-you (or future-me) recovers context. |
|
||||
| `/sync` | Pull + push changes to/from Gitea | Start of session (get latest), end of session (push yours) |
|
||||
| `/context` | Searches session logs and credentials for previous work | "What did we do for Dataforth last week?" or "What's the password for AD2?" |
|
||||
| `/checkpoint` | Git commit + database context save | After completing a feature or fix |
|
||||
| `/scc` | Save + Commit + Push (all three in one shot) | Quick end-of-session wrap-up |
|
||||
| `/1password` | Access secrets from 1Password | When vault doesn't have a credential |
|
||||
|
||||
### Why these exist
|
||||
|
||||
Without `/save`, you'd lose everything when a session ends. Without `/sync`, your work stays on one machine. Without `/context`, you'd re-discover the same information every session. These three commands are 90% of daily usage.
|
||||
|
||||
---
|
||||
|
||||
## The SOPS vault (how credentials work)
|
||||
|
||||
We store ALL credentials in an encrypted vault (separate git repo). Files are YAML encrypted with age/SOPS. Claude can decrypt them on the fly.
|
||||
|
||||
**How Claude accesses a credential:**
|
||||
```bash
|
||||
# Always via the ClaudeTools wrapper — never a hardcoded path
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh" get-field clients/dataforth/ad2.sops.yaml credentials.password
|
||||
```
|
||||
|
||||
**Why this matters:**
|
||||
- We never hardcode passwords in scripts or session logs (they're vault references)
|
||||
- The vault syncs across machines via Gitea (same as claudetools)
|
||||
- Encryption uses an age key — this key needs to be on each machine that decrypts
|
||||
|
||||
**Setup required on each machine:**
|
||||
|
||||
1. **Clone the vault repo** somewhere convenient (e.g., `~/vault` on Mac/Linux, `D:\vault` on Windows)
|
||||
|
||||
2. **Add `vault_path` to `.claude/identity.json`** (created during onboarding):
|
||||
```json
|
||||
{
|
||||
"user": "howard",
|
||||
"vault_path": "/Users/howard/vault"
|
||||
}
|
||||
```
|
||||
This is the only place the path lives — no hardcoded paths in any shared file.
|
||||
|
||||
3. **Install your age key.** Mike will give you the key file. Drop it at:
|
||||
- **Windows:** `C:\Users\<you>\AppData\Roaming\sops\age\keys.txt`
|
||||
- **Mac/Linux:** `~/.config/sops/age/keys.txt`
|
||||
|
||||
Without the age key, vault commands fail. Everything else works fine.
|
||||
|
||||
---
|
||||
|
||||
## How Claude knows about our infrastructure
|
||||
|
||||
### CLAUDE.md (the brain)
|
||||
|
||||
`.claude/CLAUDE.md` is the master instructions file. Claude reads it at the start of every session. It tells Claude:
|
||||
|
||||
- **Who we are** (AZ Computer Guru, MSP)
|
||||
- **How to behave** (delegate to agents, no emojis, use vault for creds)
|
||||
- **What projects exist** (GuruRMM, Dataforth, ClaudeTools API)
|
||||
- **How to load context** automatically when you mention a project keyword
|
||||
|
||||
**Key behavior:** If you say "work on Dataforth", Claude automatically reads `projects/dataforth-dos/CONTEXT.md` before responding. Same for "GuruRMM" → reads `projects/msp-tools/guru-rmm/CONTEXT.md`. This means Claude starts every project conversation with full context — server IPs, current state, recent work, anti-patterns to avoid.
|
||||
|
||||
### CONTEXT.md files (per-project state)
|
||||
|
||||
Each major project has a `CONTEXT.md` that captures:
|
||||
- Server IPs, ports, credentials references
|
||||
- Current deployment state
|
||||
- Recent session logs (what was done last)
|
||||
- Anti-patterns (things NOT to do, learned from past mistakes)
|
||||
- What to work on next
|
||||
|
||||
These files are the **single source of truth** for "where are we on this project."
|
||||
|
||||
### Session logs (the history)
|
||||
|
||||
Every significant work session gets a log saved to `session-logs/` (root for general, or `projects/*/session-logs/` for project-specific). These include:
|
||||
- What was accomplished
|
||||
- Full credentials used (unredacted — needed for future sessions)
|
||||
- Infrastructure changes made
|
||||
- Commands that worked and errors that didn't
|
||||
- What's still pending
|
||||
|
||||
**This is why `/save` matters.** Without it, the next person (or the next Claude session) starts from scratch.
|
||||
|
||||
---
|
||||
|
||||
## Skills (auto-invoked behaviors)
|
||||
|
||||
Skills are more powerful than commands — some trigger automatically.
|
||||
|
||||
| Skill | Auto-invokes? | What it does |
|
||||
|-------|--------------|-------------|
|
||||
| `frontend-design` | YES — after any UI change | Validates visual correctness, accessibility, design quality |
|
||||
| `stop-slop` | YES — always active | Prevents generic/lazy AI output. Enforces quality. |
|
||||
| `remediation-tool` | When you say "remediation tool" or "365" | M365 tenant investigation via our Graph API app |
|
||||
| `skill-creator` | On request | Helps build new custom skills |
|
||||
| `theme-factory` | On request | Apply visual themes to HTML artifacts |
|
||||
|
||||
### Why "stop-slop" exists
|
||||
|
||||
Without it, Claude defaults to generic patterns (purple gradients, Inter font, emoji-heavy prose). Our `stop-slop` skill enforces our standards: ASCII markers instead of emojis, specific rather than vague, no filler phrases.
|
||||
|
||||
---
|
||||
|
||||
## Agents (specialized workers)
|
||||
|
||||
Claude Code can spawn sub-agents for specific tasks. These are defined in `.claude/agents/`. The main ones you'll encounter:
|
||||
|
||||
| Agent | What it does | When Claude uses it |
|
||||
|-------|-------------|-------------------|
|
||||
| **Database Agent** | Runs SQL queries on our databases | Any database operation — Claude should NEVER query directly |
|
||||
| **Code Review Agent** | Reviews code changes for quality/security | After any code modification |
|
||||
| **Coding Agent** | Writes production code | When Claude needs to generate code (not just edit) |
|
||||
| **Explore Agent** | Searches codebases quickly | When looking for files, patterns, or understanding code |
|
||||
| **Gitea Agent** | Git commits, pushes, branch operations | Commit workflow |
|
||||
| **Backup Agent** | Backup operations | Before destructive changes |
|
||||
|
||||
**Why agents?** Claude has a limited context window. If it does everything itself, it runs out of memory mid-conversation. Agents handle heavy work in isolation and return just the summary. Also: separation of concerns — the Code Review Agent can independently evaluate code the Coding Agent wrote.
|
||||
|
||||
---
|
||||
|
||||
## Local AI tools (when available)
|
||||
|
||||
### Ollama (local LLM)
|
||||
|
||||
Ollama runs AI models locally on your GPU. Used for tasks that don't need Claude's full reasoning power — summarization, classification, data extraction.
|
||||
|
||||
**Models we use:**
|
||||
- `qwen3:14b` — general purpose (summarization, drafting)
|
||||
- `codestral:22b` — code generation assistance
|
||||
- `nomic-embed-text` — embeddings for semantic search
|
||||
|
||||
**Ollama runs on Mike's workstation** and is shared via Tailscale. You don't need to install it locally.
|
||||
|
||||
**To use from your machine (Tailscale must be connected):**
|
||||
```bash
|
||||
curl -s http://100.92.127.64:11434/api/tags
|
||||
```
|
||||
|
||||
If that returns models, you're connected. Claude automatically uses the right URL based on which machine you're on (reads from `identity.json`).
|
||||
|
||||
If it fails: check that Tailscale is connected (`tailscale status`) and Mike's workstation is online.
|
||||
|
||||
### GrepAI (semantic code search)
|
||||
|
||||
Searches code by MEANING rather than exact text. "How does auth work?" finds authentication code even if the word "auth" doesn't appear.
|
||||
|
||||
**Status:** Requires setup per-machine (index build). The `deep-explore` agent uses it. If it's not installed, Claude uses regular grep (still works, just less smart).
|
||||
|
||||
---
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
D:\claudetools\
|
||||
.claude/ — Claude's brain (CLAUDE.md, agents, skills, memory, commands)
|
||||
session-logs/ — General work logs
|
||||
projects/
|
||||
dataforth-dos/ — Dataforth test datasheet pipeline (AD2, testdatadb)
|
||||
msp-tools/
|
||||
guru-rmm/ — GuruRMM agent + server (Rust, our product)
|
||||
newsletter/ — Marketing newsletters
|
||||
clients/
|
||||
dataforth/ — Dataforth-specific client docs
|
||||
pavon/ — Pavon/client docs
|
||||
... — Other clients
|
||||
credentials.md — Quick-reference credentials (vault is source of truth)
|
||||
CONTEXT.md — Root-level project context
|
||||
|
||||
D:\vault\ — SOPS-encrypted credentials (separate repo)
|
||||
infrastructure/ — Our servers (Jupiter, Uranus, pfSense, etc.)
|
||||
clients/ — Client credentials
|
||||
services/ — Service credentials (Cloudflare, Azure, Gitea, etc.)
|
||||
projects/ — Project-specific secrets
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Daily workflow
|
||||
|
||||
### Starting a work session
|
||||
1. Open Claude Code in the project directory
|
||||
2. Claude greets you by name (reads identity.json)
|
||||
3. Tell Claude what you're working on — it auto-loads the right context
|
||||
4. Work normally — ask questions, make changes, run commands
|
||||
|
||||
### Ending a work session
|
||||
1. `/save` — creates the session log (DO THIS EVERY TIME)
|
||||
2. `/sync` — pushes everything to Gitea
|
||||
3. Close Claude Code
|
||||
|
||||
### When switching projects mid-session
|
||||
Just say "let's work on GuruRMM" or "switch to Dataforth" — Claude reads the relevant CONTEXT.md and picks up where the last session left off.
|
||||
|
||||
---
|
||||
|
||||
## Things to know
|
||||
|
||||
**Claude remembers across sessions** — via session logs and memory files, not magic. If you don't `/save`, the next session starts cold.
|
||||
|
||||
**Credentials are in the vault** — don't ask Mike for passwords; ask Claude. It decrypts from the vault.
|
||||
|
||||
**Git commits are attributed to YOU** — your name and email appear on every commit from your machine.
|
||||
|
||||
**Production deployments need care** — Claude will warn before destructive operations (git push --force, database drops, service restarts). Read the warnings.
|
||||
|
||||
**If Claude seems confused about a project** — say `/context` and ask it to search for recent work. Or read the project's CONTEXT.md yourself.
|
||||
|
||||
**If something breaks** — session logs have the full history. `git log` shows what changed and who changed it. Gitea keeps everything.
|
||||
|
||||
---
|
||||
|
||||
## Getting help
|
||||
|
||||
- Ask Claude: "What commands do I have?" or "How do I access credentials?"
|
||||
- Read `.claude/CLAUDE.md` for the full rulebook
|
||||
- Check `session-logs/` for recent work examples
|
||||
- Ask Mike
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2026-04-16*
|
||||
42
.claude/PROJECT_STATE_PROTOCOL.md
Normal file
42
.claude/PROJECT_STATE_PROTOCOL.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# PROJECT_STATE.md Locking Protocol
|
||||
|
||||
This protocol prevents conflicts between concurrent Claude sessions. Follow it for every significant action on any project that has a PROJECT_STATE.md.
|
||||
|
||||
## What Requires a Lock
|
||||
|
||||
- Editing or creating source code files
|
||||
- Git commit or push
|
||||
- SSH command that modifies a server (deploy, install, config change, service restart)
|
||||
- Database schema change or data migration
|
||||
- Build pipeline modification
|
||||
|
||||
Reading files, planning, and answering questions do NOT require a lock.
|
||||
|
||||
## The Protocol
|
||||
|
||||
**Step 1 — Read before acting**
|
||||
Re-read PROJECT_STATE.md before starting:
|
||||
- Check Active Session Locks: is anything locked that you need to touch?
|
||||
- Conflicting lock < 2 hours old: stop, report to user, ask how to proceed.
|
||||
- Lock > 2 hours old (stale): note it to user, clear the row, proceed.
|
||||
|
||||
**Step 2 — Claim your lock**
|
||||
Add a row to Active Session Locks before performing the action:
|
||||
|
||||
| Session | Working On | Status | Blocks | Started |
|
||||
|---------|-----------|--------|--------|---------|
|
||||
| DESKTOP-0O8A1RL/Claude | Brief description | IN_PROGRESS | What others must avoid | HH:MM UTC |
|
||||
|
||||
Use `{machine}/{Claude or agent description}` as the Session identifier.
|
||||
|
||||
**Step 3 — Perform the action**
|
||||
|
||||
**Step 4 — Update on completion OR failure**
|
||||
1. Remove your lock row
|
||||
2. Add a Recent Changes entry with status: `COMPLETE`, `FAILED`, `PARTIAL`, or `ROLLED_BACK`
|
||||
3. Update Current Project State if any component status changed
|
||||
4. Check off completed Pending items
|
||||
|
||||
## Stale Lock Rule
|
||||
|
||||
A lock older than 2 hours with no timestamp update is abandoned. Clear it, note `[Cleared stale lock from {session}]` in Recent Changes, then proceed.
|
||||
217
.claude/URGENT-vault-path-bug.md
Normal file
217
.claude/URGENT-vault-path-bug.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# URGENT: Vault Path Variable Collision Bug
|
||||
|
||||
**Date:** 2026-04-21
|
||||
**Severity:** CRITICAL - Blocks all remediation-tool usage
|
||||
**Affected:** All machines (DESKTOP-0O8A1RL, ACG-Tech03L, Mikes-MacBook-Air)
|
||||
**Discovered on:** Mikes-MacBook-Air during vault wrapper testing
|
||||
|
||||
---
|
||||
|
||||
## TL;DR for Windows Laptop
|
||||
|
||||
**BEFORE doing Howard's vault sync task, fix this bug first:**
|
||||
|
||||
The recent vault portability changes introduced a variable name collision in `get-token.sh` that breaks token acquisition on all machines.
|
||||
|
||||
**Quick fix (2 minutes):**
|
||||
1. Open `.claude/skills/remediation-tool/scripts/get-token.sh`
|
||||
2. Rename the `VAULT_PATH` environment variable to `VAULT_ROOT_ENV`
|
||||
3. Test: `./get-token.sh grabblaw.com investigator`
|
||||
4. If working, commit fix and push
|
||||
5. THEN proceed with Howard's vault sync task
|
||||
|
||||
---
|
||||
|
||||
## Bug Details
|
||||
|
||||
### Root Cause
|
||||
|
||||
**Variable name collision in get-token.sh around line 87-95:**
|
||||
|
||||
```bash
|
||||
# PROBLEM: VAULT_PATH is used for TWO different things
|
||||
|
||||
# Line ~40-70: VAULT_PATH stores the SOPS file relative path
|
||||
case "$TIER" in
|
||||
investigator)
|
||||
CLIENT_ID="bfbc12a4-f0dd-4e12-b06d-997e7271e10c"
|
||||
VAULT_PATH="msp-tools/computerguru-security-investigator.sops.yaml" # <-- SOPS file path
|
||||
SCOPE_URL="https://graph.microsoft.com/.default"
|
||||
;;
|
||||
...
|
||||
esac
|
||||
|
||||
# Line ~87-95: VAULT_PATH is ALSO used as environment variable for vault root
|
||||
VAULT_ROOT="${VAULT_PATH:-}" # <-- BUG: This gets the SOPS path, not the vault root!
|
||||
if [[ -z "$VAULT_ROOT" && -f "$IDENTITY_FILE" ]]; then
|
||||
for py in py python3 python; do
|
||||
if command -v "$py" >/dev/null 2>&1; then
|
||||
VAULT_ROOT=$("$py" -c "import json; print(json.load(open('$IDENTITY_FILE')).get('vault_path',''))" 2>/dev/null) && break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
```
|
||||
|
||||
**Result:** `VAULT_ROOT` gets set to `msp-tools/computerguru-security-investigator.sops.yaml` instead of the vault directory path.
|
||||
|
||||
### Observed Failure
|
||||
|
||||
```bash
|
||||
$ ./get-token.sh cascadestucson.com investigator
|
||||
|
||||
ERROR: vault not found at msp-tools/computerguru-security-investigator.sops.yaml
|
||||
(check vault_path in /Users/azcomputerguru/ClaudeTools/.claude/identity.json)
|
||||
```
|
||||
|
||||
The script is checking if `msp-tools/computerguru-security-investigator.sops.yaml` exists as a directory, which fails.
|
||||
|
||||
---
|
||||
|
||||
## Remediation Steps
|
||||
|
||||
### Step 1: Fix Variable Name Collision
|
||||
|
||||
**File:** `.claude/skills/remediation-tool/scripts/get-token.sh`
|
||||
|
||||
**Find (around line 87):**
|
||||
```bash
|
||||
VAULT_ROOT="${VAULT_PATH:-}"
|
||||
```
|
||||
|
||||
**Replace with:**
|
||||
```bash
|
||||
VAULT_ROOT="${VAULT_ROOT_ENV:-}"
|
||||
```
|
||||
|
||||
**And update the error message (around line 95):**
|
||||
```bash
|
||||
[[ -z "$VAULT_ROOT" ]] && { echo "ERROR: vault_path not set in $IDENTITY_FILE and VAULT_ROOT_ENV env var not set" >&2; exit 3; }
|
||||
```
|
||||
|
||||
**Purpose:** Separates the SOPS file path variable (`VAULT_PATH`) from the vault root override environment variable (now `VAULT_ROOT_ENV`).
|
||||
|
||||
### Step 2: Add vault_path to identity.json
|
||||
|
||||
**File:** `.claude/identity.json` (on DESKTOP-0O8A1RL)
|
||||
|
||||
**Add this field:**
|
||||
```json
|
||||
{
|
||||
"user": "mike",
|
||||
"full_name": "Mike Swanson",
|
||||
"email": "mike@azcomputerguru.com",
|
||||
"role": "admin",
|
||||
"machine": "DESKTOP-0O8A1RL",
|
||||
"vault_path": "D:/vault"
|
||||
}
|
||||
```
|
||||
|
||||
**On ACG-Tech03L (Howard's machine), the path is likely:**
|
||||
```json
|
||||
"vault_path": "D:/vault"
|
||||
```
|
||||
|
||||
**On Mikes-MacBook-Air (if vault is cloned later):**
|
||||
```json
|
||||
"vault_path": "/Users/azcomputerguru/vault"
|
||||
```
|
||||
|
||||
### Step 3: Test the Fix
|
||||
|
||||
**On DESKTOP-0O8A1RL:**
|
||||
```bash
|
||||
cd D:\ClaudeTools\.claude\skills\remediation-tool\scripts
|
||||
|
||||
# Test with a fully onboarded tenant
|
||||
bash get-token.sh grabblaw.com investigator
|
||||
|
||||
# Should output a JWT token (long string starting with eyJ...)
|
||||
# Or at least get past the vault path error
|
||||
```
|
||||
|
||||
**Expected success output:**
|
||||
```
|
||||
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6...
|
||||
```
|
||||
|
||||
**If still failing, check:**
|
||||
1. Is `D:/vault/scripts/vault.sh` present?
|
||||
2. Does `D:/vault/msp-tools/computerguru-security-investigator.sops.yaml` exist?
|
||||
3. Is SOPS configured with the correct age key?
|
||||
|
||||
### Step 4: Commit and Sync
|
||||
|
||||
```bash
|
||||
cd D:\ClaudeTools
|
||||
|
||||
git add .claude/skills/remediation-tool/scripts/get-token.sh
|
||||
git commit -m "fix: vault path variable collision in get-token.sh
|
||||
|
||||
Renamed VAULT_PATH env var to VAULT_ROOT_ENV to avoid collision with
|
||||
the SOPS file path variable. Fixes token acquisition on all machines.
|
||||
|
||||
Bug discovered during Mac testing 2026-04-21.
|
||||
|
||||
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>"
|
||||
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### Step 5: Notify Howard
|
||||
|
||||
Once fixed and pushed, tell Howard to:
|
||||
1. Pull ClaudeTools: `cd C:\ClaudeTools && git pull`
|
||||
2. Add `vault_path` to his `.claude/identity.json`
|
||||
3. Test: `bash get-token.sh grabblaw.com investigator`
|
||||
|
||||
---
|
||||
|
||||
## After This Fix - Original Vault Sync Task
|
||||
|
||||
**THEN proceed with Howard's vault sync request:**
|
||||
|
||||
1. Navigate to `D:\vault`
|
||||
2. Verify 5 new-tier SOPS files exist in `D:\vault\msp-tools\`
|
||||
3. Git add, commit, push to vault repo
|
||||
4. Notify Howard to pull vault on ACG-Tech03L
|
||||
|
||||
---
|
||||
|
||||
## Why This Happened
|
||||
|
||||
The recent portability improvements (commits 0a7cd6b and a86df11) added per-machine vault path support via `identity.json`. The implementation correctly updated `.claude/scripts/vault.sh` but inadvertently created a variable name collision in `get-token.sh` by reusing `VAULT_PATH` for both:
|
||||
- The SOPS file relative path (existing usage)
|
||||
- The environment variable override (new usage)
|
||||
|
||||
This is a regression introduced in the last 2 commits from DESKTOP-0O8A1RL.
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
After applying the fix:
|
||||
|
||||
- [ ] Token acquisition works on DESKTOP-0O8A1RL: `get-token.sh grabblaw.com investigator`
|
||||
- [ ] Token acquisition works on DESKTOP-0O8A1RL: `get-token.sh grabblaw.com investigator-exo`
|
||||
- [ ] All 5 tiers work: investigator, investigator-exo, user-manager, tenant-admin, defender
|
||||
- [ ] Vault wrapper works: `bash .claude/scripts/vault.sh list`
|
||||
- [ ] Howard can pull and test on ACG-Tech03L (after vault sync)
|
||||
- [ ] Mac can test once vault is cloned there
|
||||
|
||||
---
|
||||
|
||||
## Impact Assessment
|
||||
|
||||
**Blocked operations until fixed:**
|
||||
- All remediation-tool token acquisition
|
||||
- All breach checks via remediation-tool skill
|
||||
- Howard's Cascades spoofing hunt (double-blocked: this bug + missing SOPS files)
|
||||
- Any tenant investigation work
|
||||
|
||||
**Urgency:** Fix immediately before attempting vault sync task.
|
||||
|
||||
---
|
||||
|
||||
**Created:** 2026-04-21 19:10 (Mac session)
|
||||
**Status:** URGENT - Needs Windows laptop remediation
|
||||
**Next session:** Read this file first, apply fix, test, commit, then do vault sync
|
||||
522
.claude/VAULT-SETUP-GUIDE.md
Normal file
522
.claude/VAULT-SETUP-GUIDE.md
Normal file
@@ -0,0 +1,522 @@
|
||||
# Vault Setup Guide - Multi-Machine Reference
|
||||
|
||||
**Last Updated:** 2026-04-21
|
||||
**Tested On:** Mikes-MacBook-Air.local (Mac), DESKTOP-0O8A1RL (Windows)
|
||||
**Purpose:** Complete guide for setting up vault access on any machine
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The vault repository contains encrypted credentials (SOPS files) required for remediation-tool to acquire tokens. This guide covers full setup from scratch on any machine.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting, you need:
|
||||
- ClaudeTools repository cloned
|
||||
- Network access to Gitea server (http://172.16.3.20:3000)
|
||||
- Gitea credentials (username: azcomputerguru, password: see below)
|
||||
- Age key (private key shared across team - see below)
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference - Credentials
|
||||
|
||||
### Gitea Password
|
||||
```
|
||||
Gptf*77ttb123!@#-git
|
||||
```
|
||||
|
||||
### Age Private Key
|
||||
```
|
||||
# created: 2026-03-30T13:53:19-07:00
|
||||
# public key: age1qz7ct84m50u06h97artqddkj3c8se2yu4nxu59clq8rhj945jc0s5excpr
|
||||
AGE-SECRET-KEY-1DE3V6V0ZLLZ45A7GA77M79CTN4LZQMTRCURP8VRGNLV6T2FSZEEQXUW2EU
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### Step 1: Install Dependencies
|
||||
|
||||
**Mac (Homebrew):**
|
||||
```bash
|
||||
brew install sops age jq
|
||||
```
|
||||
|
||||
**Windows (Chocolatey):**
|
||||
```powershell
|
||||
choco install sops age jq
|
||||
```
|
||||
|
||||
**Windows (Manual):**
|
||||
- Download SOPS: https://github.com/mozilla/sops/releases
|
||||
- Download age: https://github.com/FiloSottile/age/releases
|
||||
- Download jq: https://jqlang.github.io/jq/download/
|
||||
- Add to PATH
|
||||
|
||||
**Linux (apt):**
|
||||
```bash
|
||||
sudo apt install age jq
|
||||
# SOPS from GitHub releases (not in apt)
|
||||
wget https://github.com/mozilla/sops/releases/download/v3.12.2/sops-v3.12.2.linux.amd64 -O /usr/local/bin/sops
|
||||
chmod +x /usr/local/bin/sops
|
||||
```
|
||||
|
||||
### Step 2: Clone Vault Repository
|
||||
|
||||
**Mac/Linux:**
|
||||
```bash
|
||||
git clone http://azcomputerguru@172.16.3.20:3000/azcomputerguru/vault.git ~/vault
|
||||
# Password when prompted: Gptf*77ttb123!@#-git
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```cmd
|
||||
git clone http://azcomputerguru@172.16.3.20:3000/azcomputerguru/vault.git D:\vault
|
||||
REM Password when prompted: Gptf*77ttb123!@#-git
|
||||
```
|
||||
|
||||
**Important:** Must use real terminal, not Claude Code shell (auth prompts don't work in Claude Code).
|
||||
|
||||
### Step 3: Configure Age Key
|
||||
|
||||
**Mac/Linux:**
|
||||
```bash
|
||||
mkdir -p ~/.config/sops/age
|
||||
cat > ~/.config/sops/age/keys.txt << 'AGEEOF'
|
||||
# created: 2026-03-30T13:53:19-07:00
|
||||
# public key: age1qz7ct84m50u06h97artqddkj3c8se2yu4nxu59clq8rhj945jc0s5excpr
|
||||
AGE-SECRET-KEY-1DE3V6V0ZLLZ45A7GA77M79CTN4LZQMTRCURP8VRGNLV6T2FSZEEQXUW2EU
|
||||
AGEEOF
|
||||
chmod 600 ~/.config/sops/age/keys.txt
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
$KeyDir = "$env:USERPROFILE\.config\sops\age"
|
||||
New-Item -ItemType Directory -Force -Path $KeyDir | Out-Null
|
||||
|
||||
$KeyContent = @"
|
||||
# created: 2026-03-30T13:53:19-07:00
|
||||
# public key: age1qz7ct84m50u06h97artqddkj3c8se2yu4nxu59clq8rhj945jc0s5excpr
|
||||
AGE-SECRET-KEY-1DE3V6V0ZLLZ45A7GA77M79CTN4LZQMTRCURP8VRGNLV6T2FSZEEQXUW2EU
|
||||
"@
|
||||
|
||||
Set-Content -Path "$KeyDir\keys.txt" -Value $KeyContent -NoNewline
|
||||
```
|
||||
|
||||
**Windows (Git Bash):**
|
||||
```bash
|
||||
mkdir -p /c/Users/$USER/.config/sops/age
|
||||
cat > /c/Users/$USER/.config/sops/age/keys.txt << 'AGEEOF'
|
||||
# created: 2026-03-30T13:53:19-07:00
|
||||
# public key: age1qz7ct84m50u06h97artqddkj3c8se2yu4nxu59clq8rhj945jc0s5excpr
|
||||
AGE-SECRET-KEY-1DE3V6V0ZLLZ45A7GA77M79CTN4LZQMTRCURP8VRGNLV6T2FSZEEQXUW2EU
|
||||
AGEEOF
|
||||
```
|
||||
|
||||
### Step 4: Configure SOPS Environment Variable
|
||||
|
||||
**Mac (zsh):**
|
||||
```bash
|
||||
echo 'export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt' >> ~/.zshenv
|
||||
source ~/.zshenv
|
||||
```
|
||||
|
||||
**Mac (bash):**
|
||||
```bash
|
||||
echo 'export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt' >> ~/.bash_profile
|
||||
source ~/.bash_profile
|
||||
```
|
||||
|
||||
**Windows (PowerShell - permanent):**
|
||||
```powershell
|
||||
[Environment]::SetEnvironmentVariable("SOPS_AGE_KEY_FILE", "$env:USERPROFILE\.config\sops\age\keys.txt", "User")
|
||||
```
|
||||
|
||||
**Windows (Git Bash):**
|
||||
```bash
|
||||
echo 'export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
echo 'export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
### Step 5: Fix vault.sh Line Endings (If Needed)
|
||||
|
||||
**If you see error: `env: bash\r: No such file or directory`**
|
||||
|
||||
This means vault.sh has Windows line endings (CRLF). Fix with:
|
||||
|
||||
**Mac/Linux:**
|
||||
```bash
|
||||
# Using perl (always available)
|
||||
perl -pi -e 's/\r\n/\n/g' ~/vault/scripts/vault.sh
|
||||
|
||||
# Or using dos2unix if installed
|
||||
dos2unix ~/vault/scripts/vault.sh
|
||||
```
|
||||
|
||||
**Windows (Git Bash):**
|
||||
```bash
|
||||
dos2unix /d/vault/scripts/vault.sh
|
||||
```
|
||||
|
||||
**Make executable:**
|
||||
```bash
|
||||
chmod +x ~/vault/scripts/vault.sh # Mac/Linux
|
||||
chmod +x /d/vault/scripts/vault.sh # Windows Git Bash
|
||||
```
|
||||
|
||||
### Step 6: Add vault_path to identity.json
|
||||
|
||||
**Edit ClaudeTools identity.json:**
|
||||
|
||||
**Mac:**
|
||||
```bash
|
||||
# File: ~/ClaudeTools/.claude/identity.json
|
||||
# Add this field:
|
||||
"vault_path": "/Users/azcomputerguru/vault"
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```bash
|
||||
# File: D:\ClaudeTools\.claude\identity.json
|
||||
# Add this field:
|
||||
"vault_path": "D:/vault"
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
# File: ~/ClaudeTools/.claude/identity.json
|
||||
# Add this field:
|
||||
"vault_path": "/home/<username>/vault"
|
||||
```
|
||||
|
||||
**Full example:**
|
||||
```json
|
||||
{
|
||||
"user": "mike",
|
||||
"full_name": "Mike Swanson",
|
||||
"email": "mike@azcomputerguru.com",
|
||||
"role": "admin",
|
||||
"machine": "Mikes-MacBook-Air",
|
||||
"mode": "general",
|
||||
"last_updated": "2026-04-19T08:40:00Z",
|
||||
"vault_path": "/Users/azcomputerguru/vault"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Steps
|
||||
|
||||
### Test 1: Verify SOPS Can Decrypt
|
||||
|
||||
**Mac/Linux:**
|
||||
```bash
|
||||
sops --decrypt ~/vault/msp-tools/computerguru-security-investigator.sops.yaml | head -10
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```bash
|
||||
sops --decrypt D:/vault/msp-tools/computerguru-security-investigator.sops.yaml | head -10
|
||||
```
|
||||
|
||||
**Expected output:** YAML content starting with `kind: entra-app`
|
||||
|
||||
**If you see:** `Failed to get the data key` → Age key not configured correctly
|
||||
|
||||
### Test 2: Verify vault.sh Works
|
||||
|
||||
**Mac/Linux:**
|
||||
```bash
|
||||
~/vault/scripts/vault.sh get-field msp-tools/computerguru-security-investigator.sops.yaml credentials.client_id
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```bash
|
||||
bash D:/vault/scripts/vault.sh get-field msp-tools/computerguru-security-investigator.sops.yaml credentials.client_id
|
||||
```
|
||||
|
||||
**Expected output:** `bfbc12a4-f0dd-4e12-b06d-997e7271e10c`
|
||||
|
||||
### Test 3: Verify Token Acquisition
|
||||
|
||||
**Mac/Linux:**
|
||||
```bash
|
||||
cd ~/ClaudeTools/.claude/skills/remediation-tool/scripts
|
||||
./get-token.sh grabblaw.com investigator
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```bash
|
||||
cd D:\ClaudeTools\.claude\skills\remediation-tool\scripts
|
||||
bash get-token.sh grabblaw.com investigator
|
||||
```
|
||||
|
||||
**Expected output:** JWT token starting with `eyJ0eXAiOiJKV1Qi...`
|
||||
|
||||
### Test 4: Test All Tiers
|
||||
|
||||
**Mac/Linux/Windows (Git Bash):**
|
||||
```bash
|
||||
for tier in investigator investigator-exo user-manager tenant-admin; do
|
||||
echo "Testing tier: $tier"
|
||||
./get-token.sh grabblaw.com $tier | head -c 50
|
||||
echo "..."
|
||||
echo "---"
|
||||
done
|
||||
```
|
||||
|
||||
**Expected:** JWT tokens for all 4 tiers (defender will fail - not consented in grabblaw.com)
|
||||
|
||||
---
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Issue 1: "Device not configured" when cloning vault
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
fatal: could not read Password for 'http://azcomputerguru@172.16.3.20:3000': Device not configured
|
||||
```
|
||||
|
||||
**Cause:** Git cannot prompt for password in Claude Code shell
|
||||
|
||||
**Solution:** Clone in real terminal (Terminal.app, PowerShell, etc.)
|
||||
|
||||
### Issue 2: "env: bash\r: No such file or directory"
|
||||
|
||||
**Symptom:** vault.sh won't execute, complains about `bash\r`
|
||||
|
||||
**Cause:** Windows line endings (CRLF) in vault.sh
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
perl -pi -e 's/\r\n/\n/g' ~/vault/scripts/vault.sh
|
||||
chmod +x ~/vault/scripts/vault.sh
|
||||
```
|
||||
|
||||
### Issue 3: "Failed to get the data key"
|
||||
|
||||
**Symptom:** SOPS can't decrypt files
|
||||
|
||||
**Cause:** Age key not found or SOPS_AGE_KEY_FILE not set
|
||||
|
||||
**Solution:**
|
||||
1. Verify age key exists: `cat ~/.config/sops/age/keys.txt`
|
||||
2. Set environment variable: `export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt`
|
||||
3. Add to shell RC file for persistence
|
||||
|
||||
### Issue 4: "vault_path not set in identity.json"
|
||||
|
||||
**Symptom:** get-token.sh fails with vault_path error
|
||||
|
||||
**Cause:** Missing vault_path field in .claude/identity.json
|
||||
|
||||
**Solution:** Add `"vault_path": "/path/to/vault"` to identity.json
|
||||
|
||||
### Issue 5: Python "pipefail: invalid option name"
|
||||
|
||||
**Symptom:** vault.sh fails on Mac with pipefail error
|
||||
|
||||
**Cause:** macOS ships with old bash (3.2) that doesn't support `set -o pipefail`
|
||||
|
||||
**Solution:** Already fixed in vault.sh - ensure you have latest version
|
||||
|
||||
### Issue 6: "command not found: sops"
|
||||
|
||||
**Symptom:** SOPS not in PATH
|
||||
|
||||
**Cause:** SOPS not installed or not in PATH
|
||||
|
||||
**Solution:**
|
||||
- Mac: `brew install sops`
|
||||
- Windows: `choco install sops` or add to PATH manually
|
||||
- Linux: Download from GitHub releases
|
||||
|
||||
---
|
||||
|
||||
## What Gets Installed
|
||||
|
||||
After successful setup, these files/directories exist:
|
||||
|
||||
### Mac/Linux
|
||||
```
|
||||
~/.config/sops/age/keys.txt # Age private key
|
||||
~/vault/ # Vault repository
|
||||
~/vault/.sops.yaml # SOPS config
|
||||
~/vault/msp-tools/*.sops.yaml # Encrypted credentials (6 files)
|
||||
~/vault/scripts/vault.sh # Vault CLI wrapper
|
||||
~/ClaudeTools/.claude/identity.json # Contains vault_path
|
||||
~/.zshenv (or ~/.bashrc) # Contains SOPS_AGE_KEY_FILE
|
||||
```
|
||||
|
||||
### Windows
|
||||
```
|
||||
C:\Users\<user>\.config\sops\age\keys.txt # Age private key
|
||||
D:\vault\ # Vault repository
|
||||
D:\vault\.sops.yaml # SOPS config
|
||||
D:\vault\msp-tools\*.sops.yaml # Encrypted credentials (6 files)
|
||||
D:\vault\scripts\vault.sh # Vault CLI wrapper
|
||||
D:\ClaudeTools\.claude\identity.json # Contains vault_path
|
||||
Environment variable: SOPS_AGE_KEY_FILE # System environment
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Vault Repository Structure
|
||||
|
||||
```
|
||||
vault/
|
||||
├── .sops.yaml # SOPS encryption config
|
||||
├── README.md # Vault documentation
|
||||
├── scripts/
|
||||
│ ├── vault.sh # CLI wrapper
|
||||
│ └── yaml-query.py # YAML parser (yq fallback)
|
||||
├── msp-tools/
|
||||
│ ├── computerguru-security-investigator.sops.yaml # Tier 1: Graph read
|
||||
│ ├── computerguru-exchange-operator.sops.yaml # Tier 2: EXO write
|
||||
│ ├── computerguru-user-manager.sops.yaml # Tier 3: Graph user write
|
||||
│ ├── computerguru-tenant-admin.sops.yaml # Tier 4: Graph admin
|
||||
│ ├── computerguru-defender-addon.sops.yaml # Tier 5: MDE only
|
||||
│ └── computerguru-management.sops.yaml # Legacy (deprecated)
|
||||
├── infrastructure/
|
||||
├── clients/
|
||||
├── services/
|
||||
└── projects/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
### Age Key Security
|
||||
|
||||
**The age private key decrypts ALL vault secrets. Treat it like a master password.**
|
||||
|
||||
- Never commit to git repositories
|
||||
- Never share in plaintext over unsecured channels
|
||||
- File permissions: 600 (owner read/write only)
|
||||
- Store in `.config/sops/age/` (standard location)
|
||||
|
||||
### Gitea Credentials
|
||||
|
||||
- Password: `Gptf*77ttb123!@#-git`
|
||||
- Used for vault repo clone/pull/push
|
||||
- Same credentials on all machines
|
||||
- Consider using SSH keys instead of HTTPS for better security
|
||||
|
||||
### SOPS Files
|
||||
|
||||
- Encrypted at rest with age
|
||||
- Only `credentials`, `password`, `secret`, `api_key`, `token` fields are encrypted
|
||||
- Metadata (kind, name, description) is plaintext
|
||||
- Encrypted regex defined in `.sops.yaml`
|
||||
|
||||
---
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Pulling Latest Vault Changes
|
||||
|
||||
**Mac/Linux:**
|
||||
```bash
|
||||
cd ~/vault
|
||||
git pull origin main
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```bash
|
||||
cd D:\vault
|
||||
git pull origin main
|
||||
```
|
||||
|
||||
**Run this periodically to get:**
|
||||
- New SOPS files
|
||||
- Updated credentials
|
||||
- Vault script improvements
|
||||
|
||||
### Rotating Age Key
|
||||
|
||||
If the age key needs to be rotated:
|
||||
|
||||
1. Generate new age key: `age-keygen -o new-key.txt`
|
||||
2. Re-encrypt all SOPS files with new key
|
||||
3. Distribute new key to all machines
|
||||
4. Update `.config/sops/age/keys.txt` on each machine
|
||||
5. Update `.sops.yaml` with new public key
|
||||
|
||||
**Note:** This is a team-wide operation requiring coordination.
|
||||
|
||||
---
|
||||
|
||||
## Multi-Machine Status
|
||||
|
||||
| Machine | Vault Status | Notes |
|
||||
|---------|--------------|-------|
|
||||
| DESKTOP-0O8A1RL (Windows) | ✓ WORKING | Original setup, all tiers tested |
|
||||
| Mikes-MacBook-Air (Mac) | ✓ WORKING | Full setup completed 2026-04-21 |
|
||||
| ACG-Tech03L (Howard) | PENDING | Needs vault clone + age key setup |
|
||||
| HOWARD-HOME | PENDING | Needs vault clone + age key setup |
|
||||
|
||||
---
|
||||
|
||||
## For Howard (ACG-Tech03L Setup)
|
||||
|
||||
Howard, when you're ready to set up remediation-tool:
|
||||
|
||||
### Quick Setup (Git Bash)
|
||||
|
||||
```bash
|
||||
# 1. Clone vault
|
||||
git clone http://azcomputerguru@172.16.3.20:3000/azcomputerguru/vault.git D:/vault
|
||||
# Password: Gptf*77ttb123!@#-git
|
||||
|
||||
# 2. Install age key
|
||||
mkdir -p ~/.config/sops/age
|
||||
cat > ~/.config/sops/age/keys.txt << 'AGEEOF'
|
||||
# created: 2026-03-30T13:53:19-07:00
|
||||
# public key: age1qz7ct84m50u06h97artqddkj3c8se2yu4nxu59clq8rhj945jc0s5excpr
|
||||
AGE-SECRET-KEY-1DE3V6V0ZLLZ45A7GA77M79CTN4LZQMTRCURP8VRGNLV6T2FSZEEQXUW2EU
|
||||
AGEEOF
|
||||
|
||||
# 3. Set environment variable (PowerShell)
|
||||
# Run this in PowerShell (not Git Bash):
|
||||
[Environment]::SetEnvironmentVariable("SOPS_AGE_KEY_FILE", "$env:USERPROFILE\.config\sops\age\keys.txt", "User")
|
||||
|
||||
# 4. Add vault_path to identity.json
|
||||
# Edit C:\claudetools\.claude\identity.json
|
||||
# Add: "vault_path": "D:/vault"
|
||||
|
||||
# 5. Fix line endings if needed
|
||||
dos2unix /d/vault/scripts/vault.sh
|
||||
chmod +x /d/vault/scripts/vault.sh
|
||||
|
||||
# 6. Test
|
||||
bash C:/claudetools/.claude/skills/remediation-tool/scripts/get-token.sh grabblaw.com investigator
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- **SOPS:** https://github.com/mozilla/sops
|
||||
- **age:** https://github.com/FiloSottile/age
|
||||
- **Vault repo:** http://172.16.3.20:3000/azcomputerguru/vault
|
||||
- **ClaudeTools repo:** http://172.16.3.20:3000/azcomputerguru/claudetools
|
||||
|
||||
---
|
||||
|
||||
**Last tested:** 2026-04-21 on Mikes-MacBook-Air.local
|
||||
**Status:** Complete and validated - all 4 tiers working
|
||||
**Maintainer:** Mike Swanson
|
||||
132
.claude/commands/import.md
Normal file
132
.claude/commands/import.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# /import — Ingest a folder into ClaudeTools
|
||||
|
||||
Import any folder of data into the ClaudeTools structure. Claude analyzes each file's content, classifies it, proposes placement, sanitizes credentials, and organizes everything into the correct locations.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/import <path> Import a folder
|
||||
/import <path> --dry-run Show plan without executing
|
||||
/import <path> --client <name> Hint: this data belongs to a specific client
|
||||
/import <path> --project <name> Hint: this data belongs to a specific project
|
||||
```
|
||||
|
||||
## Arguments
|
||||
|
||||
The first argument is a folder path to ingest. Everything inside (recursive) is scanned and classified.
|
||||
|
||||
## Process
|
||||
|
||||
Follow these steps IN ORDER. Do not skip any step.
|
||||
|
||||
### Step 1: Scan
|
||||
|
||||
Read the source folder recursively. For each file, note:
|
||||
- Filename + extension
|
||||
- Size
|
||||
- First ~200 lines of content (for text files)
|
||||
- Binary vs text detection
|
||||
|
||||
Skip files >50 MB (flag them for manual review).
|
||||
|
||||
### Step 2: Classify
|
||||
|
||||
For each file, determine its category based on content analysis:
|
||||
|
||||
| Category | Signals | Destination |
|
||||
|---|---|---|
|
||||
| **Session log** | Conversation transcript, dated entries, "accomplished", "session" | `session-logs/` or `projects/*/session-logs/` or `clients/*/session-logs/` |
|
||||
| **Client work** | Client name mentioned, ticket/case references, client-specific infra | `clients/<client>/` |
|
||||
| **Project code** | Source code, configs, build files, READMEs | `projects/<project>/` |
|
||||
| **Credentials** | Passwords, API keys, tokens, connection strings, SSH keys | `D:\vault\` (SOPS encrypted) |
|
||||
| **Infrastructure docs** | Server configs, network diagrams, IP lists, runbooks | `credentials.md` update or memory entry |
|
||||
| **Tool/script** | Standalone utility, automation script, helper | `tools/` or `projects/msp-tools/` |
|
||||
| **Documentation** | Guides, how-tos, notes, procedures | Project-specific docs or root docs |
|
||||
| **Unknown** | Can't classify | Flag for user decision |
|
||||
|
||||
If `--client` or `--project` was specified, weight classification toward that target.
|
||||
|
||||
### Step 3: Credential extraction
|
||||
|
||||
Before placing ANY file, scan for sensitive data:
|
||||
- Passwords (inline, in configs, in notes)
|
||||
- API keys / tokens (any string matching `[A-Za-z0-9_\-]{20,}` near words like key/token/secret)
|
||||
- Connection strings (jdbc:, postgres://, mysql://, mongodb://)
|
||||
- SSH private keys (`-----BEGIN`)
|
||||
- Certificate private keys
|
||||
|
||||
For each credential found:
|
||||
1. Show the user: "Found credential in `<file>`: `<context>` — move to vault?"
|
||||
2. If approved: create a vault SOPS entry, replace inline value with a vault reference
|
||||
3. If declined: leave as-is but warn
|
||||
|
||||
### Step 4: Present plan
|
||||
|
||||
Show a table:
|
||||
|
||||
```
|
||||
SOURCE → DESTINATION ACTION
|
||||
────────────────────────────────────────────────────────────────────────────────────
|
||||
notes/client-acme.md → clients/acme/notes.md copy
|
||||
scripts/backup-check.ps1 → tools/backup-check.ps1 copy
|
||||
creds.txt → D:\vault\clients\acme.sops.yaml vault + delete source
|
||||
session-2026-04-10.md → clients/acme/session-logs/2026-04-10.md copy
|
||||
my-tool/src/main.rs → projects/msp-tools/howard-tools/src/ copy (new project)
|
||||
random-binary.exe → (SKIP - 85 MB, too large) flag
|
||||
unknown-doc.pdf → (UNKNOWN - needs your input) ask
|
||||
```
|
||||
|
||||
Ask: "Does this plan look right? I can adjust any placement before executing."
|
||||
|
||||
### Step 5: Execute
|
||||
|
||||
After approval:
|
||||
1. Copy files to destinations (never move from source — source is the user's data)
|
||||
2. Create destination directories as needed
|
||||
3. Encrypt credential files via SOPS
|
||||
4. Update `MEMORY.md` if new knowledge was gained
|
||||
5. Update project `CONTEXT.md` files if project state changed
|
||||
6. Update `credentials.md` if infrastructure details were discovered
|
||||
|
||||
### Step 6: Report
|
||||
|
||||
Write a summary showing:
|
||||
- Files imported: N
|
||||
- Credentials vaulted: N
|
||||
- New directories created: list
|
||||
- Skipped files: list with reasons
|
||||
- Suggested follow-ups (e.g., "review clients/acme/ for completeness")
|
||||
|
||||
Commit the imported files with message: `import: ingested <N> files from <source_path>`
|
||||
|
||||
## Special cases
|
||||
|
||||
### Claude Code session data (~/.claude/projects/)
|
||||
|
||||
If the source folder IS a Claude Code projects directory (contains `.jsonl` files):
|
||||
- Use `tools/import-sessions.py` to extract summaries first
|
||||
- Then apply the standard classification to the summaries
|
||||
- Don't import raw JSONL (too large, mostly noise)
|
||||
|
||||
### Existing project detection
|
||||
|
||||
If imported code has a `Cargo.toml`, `package.json`, `pyproject.toml`, or similar:
|
||||
- Detect the project name from the manifest
|
||||
- Check if it already exists under `projects/`
|
||||
- If new: propose creating a new project directory
|
||||
- If existing: propose merging into the existing project
|
||||
|
||||
### Duplicate detection
|
||||
|
||||
Before copying, check if a file with the same name already exists at the destination:
|
||||
- If content is identical: skip (report as "already present")
|
||||
- If content differs: ask user which version to keep, or keep both with suffix
|
||||
|
||||
## File placement rules
|
||||
|
||||
Follow the conventions in `.claude/FILE_PLACEMENT_GUIDE.md`. Key rules:
|
||||
- Dataforth work → `projects/dataforth-dos/`
|
||||
- GuruRMM work → `projects/msp-tools/guru-rmm/`
|
||||
- Client work → `clients/<client-name>/`
|
||||
- General session logs → `session-logs/`
|
||||
- Credentials → SOPS vault at `D:\vault\`, NEVER in plaintext in the repo
|
||||
68
.claude/commands/mode.md
Normal file
68
.claude/commands/mode.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# /mode — Set or view the current work mode
|
||||
|
||||
Manually set the work mode, or let it auto-detect. Mode controls the terminal color and adjusts Claude's operational posture.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/mode Show current mode
|
||||
/mode client Switch to client mode (orange)
|
||||
/mode dev Switch to development mode (cyan)
|
||||
/mode infra Switch to infrastructure mode (red)
|
||||
/mode general Switch to general mode (blue)
|
||||
/mode remediation Switch to remediation/365 mode (purple)
|
||||
/mode auto Re-run auto-detection from current context
|
||||
```
|
||||
|
||||
## Modes
|
||||
|
||||
| Mode | Color | Posture |
|
||||
|---|---|---|
|
||||
| **client** | orange | Working on/for a specific client. Extra care with data handling. Session logs go to `clients/<name>/session-logs/`. Credential access audited. Always identify the client in session logs. |
|
||||
| **dev** | cyan | Building features, writing code, testing. Delegate freely to Coding/Testing agents. Use Ollama for drafts when available. Less confirmation friction on non-destructive operations. |
|
||||
| **infra** | red | Infrastructure work — servers, firewalls, DNS, deployments, backups. Confirm before any destructive or hard-to-reverse operation. Backup-first mentality. Double-check IPs and hostnames. |
|
||||
| **general** | blue | Research, planning, documentation, email drafts, general questions. Default mode. Lightweight posture. |
|
||||
| **remediation** | purple | M365 tenant work, breach investigation, security remediation. Graph API focus. Compliance-grade language. Full audit trail. |
|
||||
|
||||
## When invoked
|
||||
|
||||
1. Set the mode in `.claude/identity.json` under a `"mode"` key
|
||||
2. Run the color change: invoke `/color <mode_color>`
|
||||
3. Confirm to user: "Mode: **<mode>** (<color>)"
|
||||
|
||||
## Auto-detection rules
|
||||
|
||||
When `/mode auto` is called, OR at session start, OR when the user shifts topics, determine mode from context:
|
||||
|
||||
**Priority order (first match wins):**
|
||||
|
||||
1. **remediation** — user said "remediation tool", "365", "breach", "tenant sweep", or `/remediation-tool` was invoked
|
||||
2. **client** — user mentions a client name (check `clients/` subdirectories for name matches), or current work is under `clients/`, or user said "for <client>"
|
||||
3. **infra** — user mentions servers by name/IP (AD2, Jupiter, Uranus, pfSense, 172.16.x.x), SSH commands, firewall rules, DNS changes, service restarts, or "deploy to production"
|
||||
4. **dev** — user mentions code, building, compiling, Rust/Python/Node, cargo, npm, GuruRMM development, writing features, testing, or current work is under `projects/`
|
||||
5. **general** — default if nothing else matches
|
||||
|
||||
**On mode change (auto or manual):**
|
||||
- Update `.claude/identity.json` with `"mode": "<mode>"`
|
||||
- **Tell the user to run `/color <color>`** — Claude cannot invoke `/color` programmatically (it's a built-in CLI command). Include the command inline so the user can copy-paste.
|
||||
- Log the transition: `[MODE] general -> infra (detected: SSH to 172.16.3.30) — run /color red`
|
||||
|
||||
**Silent auto-switching:** When auto-detection triggers a mode change mid-session, announce it as: `[MODE -> infra] /color red` — short, actionable, the user can run the color command or ignore it. Don't interrupt flow with a long explanation. If the detection seems wrong, the user can override with `/mode <correct_mode>`.
|
||||
|
||||
## Session log integration
|
||||
|
||||
Session logs should include the mode in the User section:
|
||||
|
||||
```markdown
|
||||
## User
|
||||
- **User:** Mike Swanson (mike)
|
||||
- **Machine:** DESKTOP-0O8A1RL
|
||||
- **Role:** admin
|
||||
- **Mode:** infra (red)
|
||||
```
|
||||
|
||||
If mode changed during the session, note the transitions:
|
||||
|
||||
```markdown
|
||||
- **Mode:** general → infra → dev (transitioned during session)
|
||||
```
|
||||
192
.claude/commands/remediation-tool.md
Normal file
192
.claude/commands/remediation-tool.md
Normal file
@@ -0,0 +1,192 @@
|
||||
---
|
||||
description: M365 tenant investigation + remediation via the ComputerGuru tiered MSP app suite. Breach checks, tenant sweeps, consent URLs, and gated remediation actions.
|
||||
---
|
||||
|
||||
# /remediation-tool
|
||||
|
||||
M365 investigation and remediation using the **ComputerGuru tiered MSP app suite** — five multi-tenant apps covering read-only investigation, Exchange write operations, user lifecycle management, high-privilege tenant admin, and optional Defender ATP.
|
||||
|
||||
**Default posture: READ-ONLY.** Remediation actions require explicit `YES` confirmation in chat.
|
||||
|
||||
---
|
||||
|
||||
## App Tiers (quick reference)
|
||||
|
||||
| Tier flag | App | App ID | Use for |
|
||||
|---|---|---|---|
|
||||
| `investigator` | ComputerGuru Security Investigator | `bfbc12a4` | All read-only breach checks via Graph |
|
||||
| `investigator-exo` | ComputerGuru Security Investigator | `bfbc12a4` | Exchange read: Get-InboxRule (hidden), Get-Mailbox, permissions |
|
||||
| `exchange-op` | ComputerGuru Exchange Operator | `b43e7342` | Exchange write: Set-Mailbox, Remove-InboxRule, session revoke |
|
||||
| `user-manager` | ComputerGuru User Manager | `64fac46b` | User create/disable, license assign, MFA reset, password reset |
|
||||
| `tenant-admin` | ComputerGuru Tenant Admin | `709e6eed` | App role assignments, CA policy, high-privilege directory |
|
||||
| `defender` | ComputerGuru Defender Add-on | `dbf8ad1a` | Alerts, machine risk, vuln data — MDE-licensed tenants only |
|
||||
|
||||
Pass the tier flag to `get-token.sh`:
|
||||
```bash
|
||||
bash .claude/skills/remediation-tool/scripts/get-token.sh <tenant-id> <tier>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Subcommands
|
||||
|
||||
| Form | What it does |
|
||||
|---|---|
|
||||
| `/remediation-tool check <upn>` | 10-point breach check on a single user |
|
||||
| `/remediation-tool sweep <domain>` | Tenant-wide signals (sign-ins, audits, risky users, guests) |
|
||||
| `/remediation-tool signins <domain> [--user upn] [--failed-only] [--days N]` | Ad-hoc sign-in query |
|
||||
| `/remediation-tool consent-url <domain> [--app <tier>]` | Emit admin consent URL for a tenant + app |
|
||||
| `/remediation-tool remediate <upn> <action>` | **GATED:** revoke-sessions, disable-forwarding, remove-inbox-rules, disable-account, password-reset |
|
||||
|
||||
`<domain>` accepts a tenant domain (`cascadestucson.com`), a UPN (`user@domain.com`), or a tenant GUID.
|
||||
|
||||
---
|
||||
|
||||
## Workflow Claude should follow
|
||||
|
||||
### 0. Parse invocation
|
||||
|
||||
- Extract subcommand, target, and any flags from `$ARGUMENTS`.
|
||||
- Normalize: UPN -> domain (split on `@`), domain -> look up tenant-id.
|
||||
- If the target is ambiguous or missing, ask the user once and proceed.
|
||||
|
||||
### 1. Resolve tenant ID
|
||||
|
||||
Run `bash .claude/skills/remediation-tool/scripts/resolve-tenant.sh <domain>` — returns tenant GUID via OpenID discovery. If it fails, the domain is not in Entra ID; surface the error and stop.
|
||||
|
||||
### 2. Acquire tokens (cached)
|
||||
|
||||
Use the minimum-privilege tier for the task. Most breach checks only need:
|
||||
```bash
|
||||
GT=$(bash .claude/skills/remediation-tool/scripts/get-token.sh <tenant-id> investigator)
|
||||
ET=$(bash .claude/skills/remediation-tool/scripts/get-token.sh <tenant-id> investigator-exo)
|
||||
```
|
||||
|
||||
Escalate to write tiers only for remediation:
|
||||
```bash
|
||||
# Exchange write (disable-forwarding, remove-inbox-rules)
|
||||
EXO_WRITE=$(bash .claude/skills/remediation-tool/scripts/get-token.sh <tenant-id> exchange-op)
|
||||
|
||||
# User write (revoke-sessions, disable-account, password-reset, MFA reset)
|
||||
UT=$(bash .claude/skills/remediation-tool/scripts/get-token.sh <tenant-id> user-manager)
|
||||
|
||||
# Defender (MDE tenants only)
|
||||
DT=$(bash .claude/skills/remediation-tool/scripts/get-token.sh <tenant-id> defender)
|
||||
```
|
||||
|
||||
Tokens cache at `/tmp/remediation-tool/{tenant}/{tier}.jwt` with 55-minute TTL.
|
||||
|
||||
If a token returns 403/401 on first use, check `.claude/skills/remediation-tool/references/gotchas.md` for per-tenant prerequisites and emit the appropriate consent or role-assignment link.
|
||||
|
||||
### 3. Run the requested checks
|
||||
|
||||
- **`check <upn>`** -> `bash scripts/user-breach-check.sh <tenant> <upn>`. Runs all 10 checks and dumps raw JSON to `/tmp/remediation-tool/{tenant}/user-breach/<slug>/`. Interpret against `references/checklist.md` and write report.
|
||||
|
||||
- **`sweep <domain>`** -> `bash scripts/tenant-sweep.sh <tenant>`. Pulls tenant-wide failed sign-ins (30d), successful non-US sign-ins, directory audits filtered for consent/auth-method/service-principal changes, risky users, B2B guest invites. Claude summarizes priority findings.
|
||||
|
||||
- **`signins`** — build ad-hoc `curl` against Graph `/auditLogs/signIns` with the requested filter. Use `investigator` tier.
|
||||
|
||||
- **`consent-url <domain> [--app <tier>]`** — emit the appropriate admin consent URL (see below). Default to Security Investigator (`investigator`) unless `--app` specifies another tier.
|
||||
|
||||
- **`remediate`** — see Remediation section below.
|
||||
|
||||
### 4. Write the report
|
||||
|
||||
Location: `clients/{client-slug}/reports/YYYY-MM-DD-{action}.md` (UTC date). Derive client slug from domain:
|
||||
- `cascadestucson.com` -> `cascades-tucson`
|
||||
- `grabblaw.com` -> `grabblaw`
|
||||
- Use existing `clients/<slug>/` directory if present; if no match, ask the user for the slug.
|
||||
|
||||
Use `templates/breach-report.md` as skeleton. For single-user checks, fill per-check findings from raw JSON.
|
||||
|
||||
### 5. Summarize to the user
|
||||
|
||||
Short chat summary: top findings, blocked checks (with remediation links), next actions. Save raw JSON artifact paths in the report.
|
||||
|
||||
### 6. Auto-commit
|
||||
|
||||
After writing the report, delegate to the **Gitea Agent** to commit with `Remediation report: <action> for <target>`. Do not push unless the user asks.
|
||||
|
||||
---
|
||||
|
||||
## Admin Consent URLs
|
||||
|
||||
Each app must be individually consented in each customer tenant. Consent URL format:
|
||||
|
||||
```
|
||||
https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id={app-id}&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
**Security Investigator** (read-only — consent this first):
|
||||
```
|
||||
https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id=bfbc12a4-f0dd-4e12-b06d-997e7271e10c&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
**Exchange Operator** (EXO write — consent when remediation needed):
|
||||
```
|
||||
https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id=b43e7342-5b4b-492f-890f-bb5a4f7f40e9&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
**User Manager** (user/license write):
|
||||
```
|
||||
https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id=64fac46b-8b44-41ad-93ee-7da03927576c&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
**Tenant Admin** (high-privilege — use sparingly):
|
||||
```
|
||||
https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
**Defender Add-on** (MDE-licensed tenants only):
|
||||
```
|
||||
https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id=dbf8ad1a-54f4-4bb8-8a9e-ea5b9634635b&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
The customer admin must sign in as Global Admin of that tenant and click Accept. Redirect lands on azcomputerguru.com — that is expected. Verify consent via `/servicePrincipals/{sp-id}/appRoleAssignments` (new grants should be timestamped today).
|
||||
|
||||
---
|
||||
|
||||
## Remediation (gated)
|
||||
|
||||
When the user runs `/remediation-tool remediate <upn> <action>`:
|
||||
|
||||
1. **Confirm read-only context first**: skill must have recently run `check <upn>` in this session (check `/tmp/remediation-tool/{tenant}/user-breach/<slug>/` exists). If not, tell the user to run the check first.
|
||||
2. **Display the exact action** (curl command, cmdlet name, parameters).
|
||||
3. **Require explicit `YES` in chat** — not a permission prompt. Anything else aborts.
|
||||
4. Execute via the appropriate app tier. Capture response to `/tmp/remediation-tool/{tenant}/remediation/<slug>-YYYY-MM-DDTHHMMSS.json`.
|
||||
5. Update the user's report with a `## Remediation Actions` section.
|
||||
|
||||
Allowed actions and which tier handles them:
|
||||
|
||||
| Action | App tier | API |
|
||||
|---|---|---|
|
||||
| `revoke-sessions` | `user-manager` | Graph `POST /users/{upn}/revokeSignInSessions` |
|
||||
| `disable-account` | `user-manager` | Graph `PATCH /users/{upn}` with `accountEnabled: false` |
|
||||
| `password-reset` | `user-manager` | Graph `PATCH /users/{upn}` with new `passwordProfile` |
|
||||
| `disable-forwarding` | `exchange-op` | Exchange REST `Set-Mailbox -ForwardingAddress $null -ForwardingSmtpAddress $null -DeliverToMailboxAndForward $false` |
|
||||
| `remove-inbox-rules` | `exchange-op` | Exchange REST `Remove-InboxRule` per non-default rule (ask which to keep first) |
|
||||
| `disable-smtp-auth` | `exchange-op` | Exchange REST `Set-CASMailbox -SmtpClientAuthenticationDisabled $true` |
|
||||
|
||||
---
|
||||
|
||||
## Arguments
|
||||
|
||||
`$ARGUMENTS` — the full invocation text. Parse freely; common forms:
|
||||
|
||||
- `check john.trozzi@cascadestucson.com`
|
||||
- `sweep cascadestucson.com`
|
||||
- `signins cascadestucson.com --user megan.hiatt@cascadestucson.com --failed-only --days 30`
|
||||
- `consent-url cascadestucson.com`
|
||||
- `consent-url grabblaw.com --app exchange-op`
|
||||
- `remediate megan.hiatt@cascadestucson.com revoke-sessions`
|
||||
|
||||
If the user's phrasing is loose ("check john's box at cascades", "who's being attacked"), infer intent from CONTEXT.md and session logs. Prefer asking one clarifying question to guessing.
|
||||
|
||||
---
|
||||
|
||||
## Scope and references
|
||||
|
||||
- Detailed check rubric: `.claude/skills/remediation-tool/references/checklist.md`
|
||||
- Permission/role gotchas + consent URLs: `.claude/skills/remediation-tool/references/gotchas.md`
|
||||
- Endpoint cheatsheet: `.claude/skills/remediation-tool/references/graph-endpoints.md`
|
||||
- Report template: `.claude/skills/remediation-tool/templates/breach-report.md`
|
||||
@@ -72,9 +72,32 @@ Format credentials as:
|
||||
|
||||
## After Saving
|
||||
|
||||
Before committing, emit a **Change Summary** block for the user to review:
|
||||
|
||||
```
|
||||
## Change Summary (this session)
|
||||
User: <full_name> (from .claude/identity.json)
|
||||
Machine: <HOSTNAME>
|
||||
|
||||
Files changed:
|
||||
<output of: git status --short>
|
||||
|
||||
Stats:
|
||||
<output of: git diff --stat HEAD>
|
||||
```
|
||||
|
||||
Then:
|
||||
1. Commit with message: "Session log: [brief description of work done]"
|
||||
2. Push to gitea remote (if configured)
|
||||
3. Confirm push was successful
|
||||
3. After push, emit a **Post-commit Summary**:
|
||||
- New commit SHA + message
|
||||
- Author (from `git log -1 --format='%an <%ae>'`)
|
||||
- Files in the commit (from `git show --stat HEAD`)
|
||||
4. Confirm push was successful
|
||||
|
||||
### Why the summary
|
||||
|
||||
In the multi-user setup, commits can land in `main` from either team member. Always attributing author + files makes it obvious who made what change when someone else pulls the repo. Saves re-reading diffs to figure out "wait, when did that happen?"
|
||||
|
||||
## Purpose
|
||||
|
||||
|
||||
@@ -1,29 +1,504 @@
|
||||
# /sync - Bidirectional ClaudeTools Sync
|
||||
|
||||
Run the automated sync script:
|
||||
Synchronize ClaudeTools configuration, session data, and context bidirectionally with Gitea. Ensures all machines stay perfectly in sync for seamless cross-machine workflow.
|
||||
|
||||
---
|
||||
|
||||
## IMPORTANT: Use Automated Sync Script
|
||||
|
||||
**CRITICAL:** When user invokes `/sync`, execute the automated sync script instead of manual steps.
|
||||
|
||||
**Windows:**
|
||||
```bash
|
||||
bash .claude/scripts/sync.sh
|
||||
```
|
||||
OR
|
||||
```cmd
|
||||
.claude\scripts\sync.bat
|
||||
```
|
||||
|
||||
**Mac/Linux:**
|
||||
```bash
|
||||
bash .claude/scripts/sync.sh
|
||||
```
|
||||
|
||||
The script automatically:
|
||||
1. Stages and commits local changes (if any)
|
||||
2. Fetches and pulls remote changes
|
||||
3. Pushes local changes
|
||||
4. Reports sync status
|
||||
**Why use the script:**
|
||||
- Ensures PULL happens BEFORE PUSH (prevents missing remote changes)
|
||||
- Consistent behavior across all machines
|
||||
- Proper error handling and conflict detection
|
||||
- Automated timestamping and machine identification
|
||||
- No steps can be accidentally skipped
|
||||
|
||||
After the script completes, report the 3 most recent session logs:
|
||||
```bash
|
||||
ls -t session-logs/*.md projects/*/session-logs/*.md clients/*/session-logs/*.md 2>/dev/null | head -3
|
||||
```
|
||||
**The script automatically:**
|
||||
1. Checks for local changes
|
||||
2. Commits local changes (if any)
|
||||
3. **Fetches and pulls remote changes FIRST**
|
||||
4. Pushes local changes
|
||||
5. Reports sync status
|
||||
|
||||
---
|
||||
|
||||
## What Gets Synced
|
||||
|
||||
**FROM Local TO Gitea (PUSH):**
|
||||
- Session logs: `session-logs/*.md`
|
||||
- Project session logs: `projects/*/session-logs/*.md`
|
||||
- Credentials: `credentials.md` (private repo - safe to sync)
|
||||
- Project state: `SESSION_STATE.md`
|
||||
- Commands: `.claude/commands/*.md`
|
||||
- Directives: `directives.md`
|
||||
- File placement guide: `.claude/FILE_PLACEMENT_GUIDE.md`
|
||||
- Behavioral guidelines:
|
||||
- `.claude/CODING_GUIDELINES.md` (NO EMOJIS, ASCII markers, standards)
|
||||
- `.claude/AGENT_COORDINATION_RULES.md` (delegation guidelines)
|
||||
- `.claude/agents/*.md` (agent-specific documentation)
|
||||
- `.claude/CLAUDE.md` (project context and instructions)
|
||||
- Any other `.claude/*.md` operational files
|
||||
- Any other tracked changes
|
||||
|
||||
**FROM Gitea TO Local (PULL):**
|
||||
- All of the above from other machines
|
||||
- Latest commands and configurations
|
||||
- Updated session logs from other sessions
|
||||
- Project-specific work and documentation
|
||||
|
||||
---
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Phase 1: Prepare Local Changes
|
||||
|
||||
1. **Navigate to ClaudeTools repo:**
|
||||
```bash
|
||||
cd ~/ClaudeTools # or D:\ClaudeTools on Windows
|
||||
```
|
||||
|
||||
2. **Check repository status:**
|
||||
```bash
|
||||
git status
|
||||
```
|
||||
Report number of changed/new files to user
|
||||
|
||||
3. **Stage all changes:**
|
||||
```bash
|
||||
git add -A
|
||||
```
|
||||
This includes:
|
||||
- New/modified session logs
|
||||
- Updated credentials.md
|
||||
- SESSION_STATE.md changes
|
||||
- Command updates
|
||||
- Directive changes
|
||||
- Behavioral guidelines (CODING_GUIDELINES.md, AGENT_COORDINATION_RULES.md, etc.)
|
||||
- Agent documentation
|
||||
- Project documentation
|
||||
|
||||
4. **Auto-commit local changes with timestamp:**
|
||||
```bash
|
||||
git commit -m "sync: Auto-sync from [machine-name] at [timestamp]
|
||||
|
||||
Synced files:
|
||||
- Session logs updated
|
||||
- Latest context and credentials
|
||||
- Command/directive updates
|
||||
|
||||
Machine: [hostname]
|
||||
Timestamp: [YYYY-MM-DD HH:MM:SS]
|
||||
|
||||
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
**Note:** Only commit if there are changes. If working tree is clean, skip to Phase 2.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Sync with Gitea
|
||||
|
||||
5. **Pull latest changes from Gitea:**
|
||||
```bash
|
||||
git pull origin main --rebase
|
||||
```
|
||||
|
||||
**Handle conflicts if any:**
|
||||
- Session logs: Keep both versions (rename conflicting file with timestamp)
|
||||
- credentials.md: Manual merge required - report to user
|
||||
- Other files: Use standard git conflict resolution
|
||||
|
||||
Report what was pulled from remote
|
||||
|
||||
6. **Push local changes to Gitea:**
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
Confirm push succeeded
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Apply Configuration Locally
|
||||
|
||||
7. **Copy commands to global Claude directory:**
|
||||
```bash
|
||||
mkdir -p ~/.claude/commands
|
||||
cp -r ~/ClaudeTools/.claude/commands/* ~/.claude/commands/
|
||||
```
|
||||
These slash commands are now available globally
|
||||
|
||||
8. **Apply global settings if available:**
|
||||
```bash
|
||||
if [ -f ~/ClaudeTools/.claude/settings.json ]; then
|
||||
cp ~/ClaudeTools/.claude/settings.json ~/.claude/settings.json
|
||||
fi
|
||||
```
|
||||
|
||||
9. **Sync project settings:**
|
||||
```bash
|
||||
if [ -f ~/ClaudeTools/.claude/settings.local.json ]; then
|
||||
# Read and note any project-specific settings
|
||||
fi
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Context Recovery
|
||||
|
||||
10. **Find and read most recent session logs:**
|
||||
|
||||
Check all locations:
|
||||
- `~/ClaudeTools/session-logs/*.md` (general)
|
||||
- `~/ClaudeTools/projects/*/session-logs/*.md` (project-specific)
|
||||
|
||||
Report the 3 most recent logs found:
|
||||
- File name and location
|
||||
- Last modified date
|
||||
- Brief summary of what was worked on (from first 5 lines)
|
||||
|
||||
11. **Read behavioral guidelines and directives:**
|
||||
```bash
|
||||
cat ~/ClaudeTools/directives.md
|
||||
cat ~/ClaudeTools/.claude/CODING_GUIDELINES.md
|
||||
cat ~/ClaudeTools/.claude/AGENT_COORDINATION_RULES.md
|
||||
```
|
||||
Internalize operational directives and behavioral rules to ensure:
|
||||
- Proper coordination mode (delegate vs execute)
|
||||
- NO EMOJIS rule enforcement
|
||||
- Agent delegation patterns
|
||||
- Coding standards compliance
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Report Sync Status
|
||||
|
||||
12. **Summarize what was synced:**
|
||||
|
||||
```
|
||||
## Sync Complete
|
||||
|
||||
[OK] Local changes pushed to Gitea:
|
||||
- X session logs updated
|
||||
- credentials.md synced
|
||||
- SESSION_STATE.md updated
|
||||
- Y command files
|
||||
|
||||
[OK] Remote changes pulled from Gitea:
|
||||
- Z files updated from other machines
|
||||
- Latest session: [most recent log]
|
||||
|
||||
[OK] Configuration applied:
|
||||
- Commands available: /checkpoint, /context, /save, /sync, etc.
|
||||
- Directives internalized (coordination mode, delegation rules)
|
||||
- Behavioral guidelines internalized (NO EMOJIS, ASCII markers, coding standards)
|
||||
- Agent coordination rules applied
|
||||
- Global settings applied
|
||||
|
||||
Recent work (last 3 sessions):
|
||||
1. [date] - [project] - [brief summary]
|
||||
2. [date] - [project] - [brief summary]
|
||||
3. [date] - [project] - [brief summary]
|
||||
|
||||
**Status:** All machines in sync. Ready to continue work.
|
||||
```
|
||||
|
||||
13. **Refresh directives (auto-invoke):**
|
||||
|
||||
Automatically invoke `/refresh-directives` to internalize all synced behavioral guidelines:
|
||||
- Re-read directives.md
|
||||
- Re-read CODING_GUIDELINES.md
|
||||
- Re-read AGENT_COORDINATION_RULES.md
|
||||
- Perform self-assessment for violations
|
||||
- Commit to following all behavioral rules
|
||||
|
||||
**Why this is critical:**
|
||||
- Ensures latest behavioral rules are active
|
||||
- Prevents shortcut-taking after sync
|
||||
- Maintains coordination discipline
|
||||
- Enforces NO EMOJIS and ASCII marker rules
|
||||
- Ensures proper agent delegation
|
||||
|
||||
---
|
||||
|
||||
## Conflict Resolution
|
||||
|
||||
- **Session logs:** Keep both, rename with machine suffix
|
||||
- **credentials.md:** Do NOT auto-merge, report to user
|
||||
- **Other files:** Standard git conflict resolution
|
||||
### Session Log Conflicts
|
||||
If both machines created session logs with same date:
|
||||
1. Keep both versions
|
||||
2. Rename to: `YYYY-MM-DD-session-[machine].md`
|
||||
3. Report conflict to user
|
||||
|
||||
### credentials.md Conflicts
|
||||
If credentials.md has conflicts:
|
||||
1. Do NOT auto-merge
|
||||
2. Report conflict to user
|
||||
3. Show conflicting sections
|
||||
4. Ask user which version to keep or how to merge
|
||||
|
||||
### Other File Conflicts
|
||||
Standard git conflict markers:
|
||||
1. Report files with conflicts
|
||||
2. Show conflict sections
|
||||
3. Ask user to resolve manually or provide guidance
|
||||
|
||||
---
|
||||
|
||||
## Machine Detection
|
||||
|
||||
Automatically detect machine name for commit messages:
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
$env:COMPUTERNAME
|
||||
```
|
||||
|
||||
**Mac/Linux:**
|
||||
```bash
|
||||
hostname
|
||||
```
|
||||
|
||||
**Timestamp format:**
|
||||
```bash
|
||||
date "+%Y-%m-%d %H:%M:%S"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Benefits
|
||||
|
||||
### Seamless Multi-Machine Workflow
|
||||
- Start work on one machine, continue on another
|
||||
- All session context automatically synchronized
|
||||
- Credentials available everywhere (private repo)
|
||||
- Commands and directives stay consistent
|
||||
- Behavioral rules enforced identically (NO EMOJIS, delegation patterns, coding standards)
|
||||
|
||||
### Complete Context Preservation
|
||||
- Never lose session data
|
||||
- Full history across all machines
|
||||
- Searchable via git log
|
||||
- Rollback capability if needed
|
||||
|
||||
### Zero Manual Sync
|
||||
- One command syncs everything
|
||||
- Auto-commit prevents forgotten changes
|
||||
- Push/pull happens automatically
|
||||
- Conflicts handled gracefully
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Standard Sync (Most Common)
|
||||
```
|
||||
User: /sync
|
||||
|
||||
Claude:
|
||||
[Commits local changes]
|
||||
[Pulls from Gitea]
|
||||
[Pushes to Gitea]
|
||||
[Applies configuration]
|
||||
[Reports status]
|
||||
[Auto-invokes /refresh-directives]
|
||||
|
||||
Sync complete. 3 session logs pushed, 2 updates pulled.
|
||||
Directives refreshed. Ready to continue work.
|
||||
```
|
||||
|
||||
### Sync Before Important Work
|
||||
```
|
||||
User: "I'm switching to my other machine. /sync"
|
||||
|
||||
Claude:
|
||||
[Syncs everything]
|
||||
Report: Latest work on Dataforth DOS dashboard pushed to Gitea.
|
||||
All session logs and credentials synced.
|
||||
You can now pull on the other machine to continue.
|
||||
```
|
||||
|
||||
### Daily Morning Sync
|
||||
```
|
||||
User: /sync
|
||||
|
||||
Claude:
|
||||
[Pulls overnight changes from other machines]
|
||||
[Auto-invokes /refresh-directives]
|
||||
Report: Found 2 new sessions from yesterday evening.
|
||||
Latest: GuruRMM dashboard redesign completed.
|
||||
Context recovered. Directives refreshed. Ready for today's work.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
If push fails with auth error, retry once (transient Gitea auth issue).
|
||||
If pull fails with conflicts, report affected files and ask for guidance.
|
||||
### Network Issues
|
||||
If git pull/push fails:
|
||||
1. Report connection error
|
||||
2. Show what was committed locally
|
||||
3. Suggest retry or manual sync
|
||||
4. Changes are safe (committed locally)
|
||||
|
||||
### Authentication Issues
|
||||
If Gitea authentication fails:
|
||||
1. Report auth error
|
||||
2. Check SSH keys or credentials
|
||||
3. Provide troubleshooting steps
|
||||
4. Manual push may be needed
|
||||
|
||||
### Merge Conflicts
|
||||
If automatic merge fails:
|
||||
1. Report which files have conflicts
|
||||
2. Show conflict markers
|
||||
3. Ask for user guidance
|
||||
4. Offer to abort merge if needed
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
**credentials.md Syncing:**
|
||||
- Private repository on Gitea (https://git.azcomputerguru.com)
|
||||
- Only accessible to authorized user
|
||||
- Encrypted in transit (HTTPS/SSH)
|
||||
- Safe to sync sensitive credentials
|
||||
- Enables cross-machine access
|
||||
|
||||
**What's NOT synced:**
|
||||
- `.env` files (gitignored)
|
||||
- API virtual environment (api/venv/)
|
||||
- Database files (local development)
|
||||
- Temporary files (*.tmp, *.log)
|
||||
- node_modules/ directories
|
||||
|
||||
---
|
||||
|
||||
## Integration with Other Commands
|
||||
|
||||
### After /checkpoint
|
||||
User can run `/sync` after `/checkpoint` to push the checkpoint to Gitea:
|
||||
```
|
||||
User: /checkpoint
|
||||
Claude: [Creates git commit]
|
||||
|
||||
User: /sync
|
||||
Claude: [Pushes checkpoint to Gitea]
|
||||
```
|
||||
|
||||
### Before /save
|
||||
User can sync first to see latest context:
|
||||
```
|
||||
User: /sync
|
||||
Claude: [Shows latest session logs]
|
||||
|
||||
User: /save
|
||||
Claude: [Creates session log with full context]
|
||||
```
|
||||
|
||||
### With /context
|
||||
Syncing ensures `/context` has complete history:
|
||||
```
|
||||
User: /sync
|
||||
Claude: [Syncs all session logs]
|
||||
|
||||
User: /context Dataforth
|
||||
Claude: [Searches complete session log history including other machines]
|
||||
```
|
||||
|
||||
### Auto-invokes /refresh-directives
|
||||
**IMPORTANT:** `/sync` automatically invokes `/refresh-directives` at the end:
|
||||
```
|
||||
User: /sync
|
||||
Claude:
|
||||
[Phase 1: Commits local changes]
|
||||
[Phase 2: Pulls/pushes to Gitea]
|
||||
[Phase 3: Applies configuration]
|
||||
[Phase 4: Recovers context]
|
||||
[Phase 5: Reports status]
|
||||
[Auto-invokes /refresh-directives]
|
||||
[Confirms directives internalized]
|
||||
|
||||
Sync complete. Directives refreshed. Ready to coordinate.
|
||||
```
|
||||
|
||||
**Why automatic:**
|
||||
- Ensures latest behavioral rules are active after pulling changes
|
||||
- Prevents using outdated directives from previous sync
|
||||
- Maintains coordination discipline across all machines
|
||||
- Enforces NO EMOJIS rule after any directive updates
|
||||
- Critical after conversation compaction or multi-machine sync
|
||||
|
||||
---
|
||||
|
||||
## Frequency Recommendations
|
||||
|
||||
**Daily:** Start of work day
|
||||
- Pull overnight changes
|
||||
- See what was done on other machines
|
||||
- Recover latest context
|
||||
|
||||
**After Major Work:** End of coding session
|
||||
- Push session logs
|
||||
- Share context across machines
|
||||
- Backup to Gitea
|
||||
|
||||
**Before Switching Machines:**
|
||||
- Push all local changes
|
||||
- Ensure other machine can pull
|
||||
- Seamless transition
|
||||
|
||||
**Weekly:** General maintenance
|
||||
- Keep repos in sync
|
||||
- Review session log history
|
||||
- Clean up if needed
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Already up to date" but files seem out of sync
|
||||
```bash
|
||||
# Force status check
|
||||
cd ~/ClaudeTools
|
||||
git fetch origin
|
||||
git status
|
||||
```
|
||||
|
||||
### "Divergent branches" error
|
||||
```bash
|
||||
# Rebase local changes on top of remote
|
||||
git pull origin main --rebase
|
||||
```
|
||||
|
||||
### Lost uncommitted changes
|
||||
```bash
|
||||
# Check stash
|
||||
git stash list
|
||||
|
||||
# Recover if needed
|
||||
git stash pop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Created:** 2026-01-21
|
||||
**Purpose:** Bidirectional sync for seamless multi-machine ClaudeTools workflow
|
||||
**Repository:** https://git.azcomputerguru.com/azcomputerguru/claudetools.git
|
||||
**Status:** Active - comprehensive sync with context preservation
|
||||
|
||||
511
.claude/commands/syncro.md
Normal file
511
.claude/commands/syncro.md
Normal file
@@ -0,0 +1,511 @@
|
||||
# /syncro — Syncro PSA ticket management
|
||||
|
||||
Create, update, close, comment on, and bill tickets in Syncro PSA.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/syncro Show open tickets summary
|
||||
/syncro ticket <number> View ticket details + comments
|
||||
/syncro create <customer> <subject> Create new ticket
|
||||
/syncro update <number> <status> Update ticket status
|
||||
/syncro close <number> Close/resolve a ticket
|
||||
/syncro comment <number> <text> Add a comment to a ticket
|
||||
/syncro bill <number> Add billable time and create invoice
|
||||
/syncro search <query> Search tickets by subject/customer
|
||||
/syncro customers <query> Search customers
|
||||
```
|
||||
|
||||
## API Configuration
|
||||
|
||||
**Base URL:** `https://computerguru.syncromsp.com/api/v1`
|
||||
**API Key:** per-user tokens in SOPS vault — see "Get API key" below
|
||||
**Rate limit:** 180 requests/minute per IP
|
||||
**Docs:** https://api-docs.syncromsp.com/
|
||||
|
||||
## Hard Rules (violations have occurred — no exceptions)
|
||||
|
||||
**Before any POST:** Always show the full payload to the user and wait for explicit confirmation. This applies to tickets, comments, line items, and invoices — including hidden/internal notes.
|
||||
|
||||
**After any ambiguous POST result** (null fields, jq error, curl error, timeout): Do NOT retry. GET the resource first to confirm whether the action succeeded. Syncro has no idempotency on any endpoint — one POST always creates one record. Duplicate tickets and comments cannot be deleted via API; comments require manual GUI removal.
|
||||
|
||||
**Ticket response shape:** `{"ticket": {...}}` — always use `.ticket.id`, never `.id`. The flat-object jq pattern silently returns nulls and looks like failure when it isn't.
|
||||
|
||||
**Billing:** Always ask for minutes and labor type before adding any line item. Never assume a default.
|
||||
|
||||
**Emergency/after-hours billing — check prepaid first:** Before adding a `26184` (Emergency) line item, `GET /customers/<id>` and read `prepay_hours`. If `prepay_hours > 0`, the customer has a prepaid block — bill `26118` (Onsite) at `quantity × 1.5` instead (prepaid debits by quantity, not by dollars). Never stack `26118` + `26184` for the same hours — the Emergency product rate already has the 1.5× multiplier baked in. Verified 2026-04-23 on ticket #32203 (Desert Auto Tech) after Winter caught the bug.
|
||||
|
||||
**Line-item `price_retail` MUST be set explicitly:** Earlier guidance to "omit `price_retail` and let Syncro auto-calc from the product rate" was wrong — the rate does NOT populate automatically. Fetch it with `GET /products/<id>` → `.product.price_retail` and pass it on `add_line_item`. Omitting it leaves the line at $0.00 and the invoice posts at $0.00 (verified 2026-04-23 on #32203).
|
||||
|
||||
## Implementation
|
||||
|
||||
When invoked, use the Syncro REST API via `curl`. All requests include `?api_key=<key>` as query parameter (NOT in header — Syncro uses query param auth).
|
||||
|
||||
### Attribution rule (CRITICAL)
|
||||
|
||||
Every Syncro API call is attributed to the **owner of the API key**. Comments, line items, timer entries, and invoices created by the API are logged as the API user — regardless of who is running the command. So the skill MUST use a per-user API key that matches the actual tech running it, or comments will be misattributed.
|
||||
|
||||
| identity.json user | Syncro user | user_id |
|
||||
|---|---|---|
|
||||
| `mike` | Michael Swanson | 1735 |
|
||||
| `howard` | Howard Enos | 1750 |
|
||||
|
||||
Keys are baked into the skill below. To add a new user: generate a token in Syncro → Admin → API Tokens, add a case to the key-select block, and store a backup copy in the vault at `msp-tools/syncro-<user>.sops.yaml`.
|
||||
|
||||
### Get API key
|
||||
|
||||
```bash
|
||||
BASE="https://computerguru.syncromsp.com/api/v1"
|
||||
|
||||
# Per-user keys — actions in Syncro are attributed to the key owner
|
||||
USER_ID=$(jq -r '.user // empty' "$CLAUDETOOLS_ROOT/.claude/identity.json")
|
||||
case "$USER_ID" in
|
||||
mike) API_KEY="T259810e5c9917386b-52c2aeea7cdb5ff41c6685a73cebbeb3" ;;
|
||||
howard) API_KEY="Tde5174a6e9e312d14-02fd5bfe0f0ee40c87d027507c680e18" ;;
|
||||
*) echo "[ERROR] Unknown user '$USER_ID' in identity.json — cannot select Syncro API key" >&2; exit 1 ;;
|
||||
esac
|
||||
```
|
||||
|
||||
### Adding a per-user key
|
||||
|
||||
1. User logs into Syncro → Admin → API Tokens → New (`/api_tokens/new`)
|
||||
2. Type: Integration API Token (or Custom with all standard scopes: asset/customer/ticket/invoice/payment read+write+delete, worksheet add+manage+delete, chat + script.execute)
|
||||
3. Copy the token once (Syncro only shows it on creation)
|
||||
4. Encrypt to vault:
|
||||
```bash
|
||||
cat > $VAULT_ROOT/msp-tools/syncro-<user>.sops.yaml <<YAML
|
||||
kind: api-key
|
||||
name: Syncro (<Full Name>)
|
||||
subdomain: computerguru
|
||||
api-base-url: https://computerguru.syncromsp.com/api/v1
|
||||
api-docs: https://api-docs.syncromsp.com/
|
||||
status: active
|
||||
owner: <user>
|
||||
syncro_user_id: <id>
|
||||
tags: [msp-tools, per-user]
|
||||
credentials:
|
||||
credential: <TOKEN>
|
||||
notes: Per-user Syncro API token for <Full Name>. Created YYYY-MM-DD.
|
||||
YAML
|
||||
# MUST run from vault root so sops picks up .sops.yaml
|
||||
(cd "$VAULT_ROOT" && sops --encrypt --in-place "msp-tools/syncro-<user>.sops.yaml")
|
||||
```
|
||||
5. Commit + push vault repo.
|
||||
|
||||
### Endpoints reference
|
||||
|
||||
#### Tickets
|
||||
|
||||
| Operation | Method | Endpoint | Body |
|
||||
|---|---|---|---|
|
||||
| List tickets | GET | `/tickets?status=<status>&per_page=25` | — |
|
||||
| Get ticket | GET | `/tickets/<id>` | — |
|
||||
| Create ticket | POST | `/tickets` | see full create workflow below |
|
||||
| Update ticket | PUT | `/tickets/<id>` | `{"status": "In Progress", "priority": "..."}` |
|
||||
| Delete ticket | DELETE | `/tickets/<id>` | — |
|
||||
|
||||
**Ticket statuses:** `New`, `In Progress`, `Waiting on Customer`, `Waiting on Vendor`, `Scheduled`, `Resolved`, `Invoiced`, `Closed`
|
||||
|
||||
**Priority format** (number-prefixed string): `"1 High"`, `"2 Normal"`, `"3 Low"`, `"4 Urgent"`
|
||||
Default: `"2 Normal"`. Use `"4 Urgent"` for emergency/after-hours.
|
||||
|
||||
**Problem types (Issue Type dropdown — use closest match, else "Not determined"):**
|
||||
`API`, `Email`, `Emergency Service`, `File Services / Permissions`, `Hardware`, `Maintenance`,
|
||||
`New User / M365 Account Creation`, `New User / Workstation Deployment`, `Not determined`,
|
||||
`Onsite`, `Other`, `Phone/VOIP`, `Remote`, `Security`, `Server Migration`, `Service Request`,
|
||||
`Software`, `Website`
|
||||
|
||||
**Appointment types:**
|
||||
|
||||
| Name | ID | location_type |
|
||||
|---|---|---|
|
||||
| In Shop | 4321 | shop |
|
||||
| Onsite | 4322 | customer |
|
||||
| Phone Call | 4323 | pre_defined |
|
||||
| Reminder | 193053 | manual_entry |
|
||||
| Remote | 59289 | pre_defined |
|
||||
|
||||
**Tech user IDs:** Mike = 1735, Howard = 1750, Winter = 1737, Rob = 1760
|
||||
|
||||
---
|
||||
|
||||
### Ticket creation workflow (full — 3 API calls)
|
||||
|
||||
Ticket creation in Syncro maps to three separate API calls. Gather all inputs first, show a full preview, wait for confirmation, then execute in order.
|
||||
|
||||
#### Step 1 — Gather inputs
|
||||
|
||||
Collect in one pass (do not ask field by field):
|
||||
|
||||
| # | Field | Notes |
|
||||
|---|---|---|
|
||||
| 1 | **Subject** | Brief title: reason for the ticket |
|
||||
| 2 | **Issue Type** (`problem_type`) | From dropdown above; "Not determined" if unclear |
|
||||
| 3 | **Priority** | "2 Normal" default; "4 Urgent" for emergencies |
|
||||
| 4 | **Description** | Expanded detail — becomes the "Initial Issue" comment body |
|
||||
| 5 | **Do Not Email** | Suppress customer notification on ticket create? (yes for internal/reminder tickets) |
|
||||
| 6 | **Due Date** | ISO date |
|
||||
| 7 | **Assigned Tech** | Who owns the ticket |
|
||||
| 8 | **Contact** | Look up from `GET /customers/{id}` → `.contacts[]`; show list, ask user to pick |
|
||||
| 9 | **Address/Site** | `address_id` — also comes from customer contacts with address data |
|
||||
| 10 | **Appointment Type** | From table above; omit section if no appointment needed |
|
||||
| 11 | **Location** | Free text; usually blank unless onsite at non-primary address |
|
||||
| 12 | **Start Time** | ISO8601 datetime; omit if no scheduled appointment |
|
||||
| 13 | **End Time** | Default: start + 90 minutes |
|
||||
| 14 | **Appointment Owner** | Usually same as assigned tech; noted for calendar attribution (not a separate API field — inherits from ticket `user_id`) |
|
||||
| 15 | **Do Not Invite** | If not onsite, suppress calendar invite — note: not directly controllable via API; inform user if they need this set manually |
|
||||
| 16 | **Asset** | Search `GET /customer_assets?customer_id=N&query=<name>` if a specific device is involved |
|
||||
|
||||
#### Step 2 — Look up customer data
|
||||
|
||||
Before showing the preview, fetch what you need:
|
||||
|
||||
```bash
|
||||
# Get contacts and addresses
|
||||
curl -s "${BASE}/customers/${CUST_ID}?api_key=${API_KEY}" | jq '{contacts: [.customer.contacts[] | {id, name, address1, email}]}'
|
||||
|
||||
# Search assets
|
||||
curl -s "${BASE}/customer_assets?customer_id=${CUST_ID}&query=<name>&api_key=${API_KEY}" | jq '[.assets[] | {id, name, asset_type}]'
|
||||
```
|
||||
|
||||
#### Step 3 — Show preview and confirm
|
||||
|
||||
Display the full ticket before posting. Include all populated fields. Wait for explicit confirmation.
|
||||
|
||||
```
|
||||
TICKET PREVIEW
|
||||
--------------
|
||||
Customer: <name>
|
||||
Subject: <subject>
|
||||
Issue Type: <problem_type>
|
||||
Priority: <priority>
|
||||
Description: <description>
|
||||
Due Date: <due_date>
|
||||
Assigned To: <tech name>
|
||||
Contact: <contact name>
|
||||
Address: <address>
|
||||
Do Not Email: <yes/no>
|
||||
|
||||
APPOINTMENT
|
||||
-----------
|
||||
Type: <type name>
|
||||
Start: <start_at>
|
||||
End: <end_at> (90 min)
|
||||
Location: <location or blank>
|
||||
|
||||
ASSET: <asset name or none>
|
||||
|
||||
Confirm? (yes/no)
|
||||
```
|
||||
|
||||
#### Step 4 — Execute (after confirmation)
|
||||
|
||||
**Call 1 — Create ticket:**
|
||||
|
||||
```bash
|
||||
curl -s -X POST "${BASE}/tickets?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @/tmp/ticket_payload.json
|
||||
# Parse: TICKET_ID=$(... | jq -r '.ticket.id')
|
||||
# Parse: CUST_ID=$(... | jq -r '.ticket.customer_id')
|
||||
```
|
||||
|
||||
Payload fields (omit null/blank):
|
||||
```json
|
||||
{
|
||||
"customer_id": N,
|
||||
"subject": "...",
|
||||
"problem_type": "...",
|
||||
"status": "New",
|
||||
"priority": "2 Normal",
|
||||
"user_id": N,
|
||||
"due_date": "YYYY-MM-DD",
|
||||
"contact_id": N,
|
||||
"address_id": N,
|
||||
"start_at": "ISO8601",
|
||||
"end_at": "ISO8601",
|
||||
"asset_ids": [N]
|
||||
}
|
||||
```
|
||||
|
||||
**Call 2 — Post initial description as "Initial Issue" comment:**
|
||||
|
||||
```bash
|
||||
curl -s -X POST "${BASE}/tickets/${TICKET_ID}/comment?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @/tmp/comment_payload.json
|
||||
# Parse: .comment.id (NOT .id — see Hard Rules)
|
||||
```
|
||||
|
||||
Payload:
|
||||
```json
|
||||
{
|
||||
"subject": "Initial Issue",
|
||||
"body": "<the full description>",
|
||||
"hidden": false,
|
||||
"do_not_email": true
|
||||
}
|
||||
```
|
||||
Set `do_not_email: true` if "Do Not Email" was checked; `false` otherwise.
|
||||
|
||||
**Call 3 — Create appointment (only if start_at provided):**
|
||||
|
||||
```bash
|
||||
curl -s -X POST "${BASE}/appointments?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @/tmp/appt_payload.json
|
||||
```
|
||||
|
||||
Payload:
|
||||
```json
|
||||
{
|
||||
"ticket_id": N,
|
||||
"customer_id": N,
|
||||
"appointment_type_id": N,
|
||||
"start_at": "ISO8601",
|
||||
"end_at": "ISO8601",
|
||||
"location": ""
|
||||
}
|
||||
```
|
||||
|
||||
Note: "Do Not Invite" (suppress calendar invite email) is not API-controllable. Tell the user to toggle it in the Syncro GUI if needed.
|
||||
|
||||
**Always use temp files for payloads** — never inline JSON in curl -d with ticket data (special characters, newlines in description will break the shell).
|
||||
|
||||
#### Comments
|
||||
|
||||
| Operation | Method | Endpoint | Body |
|
||||
|---|---|---|---|
|
||||
| Add comment | POST | `/tickets/<id>/comment` | `{"subject": "Update", "body": "...", "hidden": false, "do_not_email": false}` |
|
||||
|
||||
**Comment fields (verified):**
|
||||
- `subject` — required; comment header (e.g., "Update", "Resolution", "Internal Note")
|
||||
- `body` — required; comment text (HTML supported)
|
||||
- `hidden` — bool; if true, internal-only (customer can't see)
|
||||
- `do_not_email` — bool; if true, suppresses customer email notification
|
||||
- `tech` — string; overrides the authenticated user's name shown on the comment
|
||||
|
||||
**Silently ignored (do not use):** `product_id`, `minutes_spent`, `bill_time_now` — accepted but not saved. Verified 2026-04-21.
|
||||
|
||||
**CRITICAL — response wrapper:** POST /comment returns `{"comment": {"id": ..., "subject": ..., ...}}` — NOT a flat object. Always parse as `.comment.id`, `.comment.created_at`, etc. Using `.id` returns null and looks like failure even when the comment posted successfully. This caused duplicate comments on 2026-04-22 (#32185) and 2026-04-23 (#32142) — both times the POST succeeded but null `.id` triggered a retry.
|
||||
|
||||
```bash
|
||||
# Correct pattern — always check .comment.id
|
||||
RESP=$(curl -s -X POST "${BASE}/tickets/${ID}/comment?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @/tmp/payload.json)
|
||||
echo "$RESP" | jq '{id: .comment.id, subject: .comment.subject, created_at: .comment.created_at}'
|
||||
```
|
||||
|
||||
**CRITICAL — duplicate prevention:** The server has no idempotency. One POST = one comment, always. Duplicates are caused by calling the endpoint twice (retry after a perceived timeout, double tool invocation, etc.). **Never retry a POST /comment without first GET /tickets/{id} to confirm the comment did not already land.** When verifying, search all comments by subject — do not rely on `[-3:]` tail. The `Idempotency-Key` header is silently ignored.
|
||||
|
||||
```bash
|
||||
# Correct verification pattern after ambiguous response
|
||||
curl -s "${BASE}/tickets/${ID}?api_key=${API_KEY}" | \
|
||||
jq '.ticket.comments[] | select(.subject == "Your Subject Here") | {id, created_at}'
|
||||
```
|
||||
|
||||
**Comments cannot be deleted via API.** No DELETE endpoint exists in the Syncro API for comments — confirmed against official swagger spec. Duplicate comments require manual removal in the GUI.
|
||||
|
||||
**Do NOT wrap body in `{"comment": {...}}`** — returns 422 "Body can't be blank". POST flat JSON directly.
|
||||
|
||||
#### Customers
|
||||
|
||||
| Operation | Method | Endpoint |
|
||||
|---|---|---|
|
||||
| List/search | GET | `/customers?query=<search>&per_page=25` |
|
||||
| Get customer | GET | `/customers/<id>` |
|
||||
| Create customer | POST | `/customers` |
|
||||
|
||||
#### Billable Line Items
|
||||
|
||||
Two verified ways to add billable time. Both produce ticket line items that transfer to invoices.
|
||||
|
||||
**Option A — Direct line item (simpler):**
|
||||
|
||||
| Operation | Method | Endpoint |
|
||||
|---|---|---|
|
||||
| Add line item | POST | `/tickets/<id>/add_line_item` |
|
||||
| Remove line item | POST | `/tickets/<id>/remove_line_item` |
|
||||
| Update line item | PUT | `/tickets/<id>/update_line_item` |
|
||||
|
||||
```bash
|
||||
# Add (always include price_retail — API does not auto-apply product rates)
|
||||
curl -s -X POST "${BASE}/tickets/${ID}/add_line_item?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"product_id": 1190473, "quantity": 0.5, "price_retail": 150.00, "name": "Labor - Remote Business", "description": "Work description", "taxable": false}'
|
||||
|
||||
# Remove
|
||||
curl -s -X POST "${BASE}/tickets/${ID}/remove_line_item?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"ticket_line_item_id": 12345}'
|
||||
# Returns: {"success": true, "message": ""}
|
||||
```
|
||||
|
||||
**Option B — Timer then charge (for time-tracking workflows):**
|
||||
|
||||
```bash
|
||||
# 1. Create timer entry
|
||||
curl -s -X POST "${BASE}/tickets/${ID}/timer_entry?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"start_at": "ISO8601", "end_at": "ISO8601", "notes": "...", "billable": true, "product_id": 1190473}'
|
||||
|
||||
# 2. Charge timer — sets recorded:true and creates linked line item
|
||||
curl -s -X POST "${BASE}/tickets/${ID}/charge_timer_entry?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"timer_entry_id": N}'
|
||||
|
||||
# Delete timer (if needed)
|
||||
curl -s -X POST "${BASE}/tickets/${ID}/delete_timer_entry?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"timer_entry_id": N}'
|
||||
# Returns: {"success": true}
|
||||
```
|
||||
|
||||
**add_line_item required fields:**
|
||||
- `name` — required (422 if missing)
|
||||
- `description` — required (422 if missing)
|
||||
- `product_id` — labor product ID (see table below)
|
||||
- `quantity` — decimal hours (0.5 = 30 min, 1.0 = 1 hour)
|
||||
- `price_retail` — **must always be set explicitly**; `price`, `retail_price`, `rate`, `price_cents` all silently ignored and leave line at $0.00. Syncro does NOT auto-calculate rates via API even though it does in the web UI. Omitting it leaves the line at $0.00 and the invoice generates at $0 (verified 2026-04-23 on #32203). Always pass the rate from the table below.
|
||||
- `taxable: false` — **always set explicitly**; labor products default to no-tax in GUI but the API applies tax if this is omitted
|
||||
|
||||
**Do NOT remove ticket line items after invoicing.** Leave them on the ticket — the "Add/View Charges" button and billing verification by techs depends on seeing line items there.
|
||||
|
||||
**Labor product IDs and rates** (rates pulled from Syncro API 2026-04-24):
|
||||
|
||||
| product_id | Name | price_retail ($/hr) | Notes |
|
||||
|---|---|---|---|
|
||||
| `1190473` | Labor - Remote Business | `150.00` | Standard remote work |
|
||||
| `26118` | Labor - Onsite Business | `175.00` | Base onsite rate |
|
||||
| `26184` | Labor - Emergency or After Hours Business | `262.50` | **1.5× onsite; time-and-a-half baked into the rate.** Non-prepaid customers only. Do NOT stack with `26118` for the same hours. |
|
||||
| `9269129` | Labor - Prepaid Project Labor | `0.00` | Debits from customer `prepay_hours` bank |
|
||||
| `9269124` | Labor - Internal Labor | `0.00` | Non-billable internal time |
|
||||
| `26117` | Fee - Travel Time | `40.00` | Per travel event (not hourly) |
|
||||
| `68055` | Labor - Website Labor | `150.00` | Website-related work |
|
||||
|
||||
`price_retail` is the per-unit rate. Line item total = `price_retail × quantity`.
|
||||
|
||||
**Emergency / after-hours billing branches by whether customer has prepaid labor:**
|
||||
|
||||
Check: `GET /customers/<id>` → `.customer.prepay_hours` (string; `"0.0"` means no prepaid, any non-zero means prepaid block exists).
|
||||
|
||||
| `prepay_hours` | Regular hours | Emergency / after-hours |
|
||||
|---|---|---|
|
||||
| `0` / null (no prepaid) | `26118`, qty = actual_hours | `26184`, qty = actual_hours (rate already 1.5×) |
|
||||
| `> 0` (has prepaid block) | `26118`, qty = actual_hours | `26118`, qty = actual_hours × **1.5** |
|
||||
|
||||
**Rationale (Winter, 2026-04-23):** Prepaid blocks debit by QUANTITY, not dollars. To charge time-and-a-half against a prepaid block we bump the quantity to 1.5× on the Onsite product rather than switching to the Emergency product — switching would double-count because the Emergency product has the 1.5× already built into its dollar rate.
|
||||
|
||||
**Example — 2 hour emergency onsite job:**
|
||||
- Non-prepaid customer: one line of 2.0 hrs × `26184` @ $262.50 → $525.00 billed
|
||||
- Prepaid customer: one line of 3.0 hrs × `26118` @ $175.00 → debits 3 hrs from prepaid block
|
||||
|
||||
Winter caught this on #32203 (Desert Auto Tech) 2026-04-23 after a stack of 1hr `26118` + 1hr `26184` for a single hour of emergency work — the $ doubled because the 1.5× was applied twice.
|
||||
|
||||
#### Timer Entries (time tracking reference)
|
||||
|
||||
| Operation | Method | Endpoint |
|
||||
|---|---|---|
|
||||
| Add timer | POST | `/tickets/<id>/timer_entry` |
|
||||
| Charge timer → line item | POST | `/tickets/<id>/charge_timer_entry` |
|
||||
| Update timer | PUT | `/tickets/<id>/update_timer_entry` |
|
||||
| Delete timer | POST | `/tickets/<id>/delete_timer_entry` |
|
||||
| List timers | GET | `/ticket_timers?ticket_id=<id>` |
|
||||
|
||||
#### Invoices
|
||||
|
||||
| Operation | Method | Endpoint | Body |
|
||||
|---|---|---|---|
|
||||
| List invoices | GET | `/invoices?per_page=25` | — |
|
||||
| Get invoice | GET | `/invoices/<id>` | — |
|
||||
| Create from ticket | POST | `/invoices` | `{"ticket_id": N, "customer_id": N, "category": "Standard"}` |
|
||||
| Delete invoice | DELETE | `/invoices/<id>` | — |
|
||||
|
||||
**"Make Invoice" flow:** `POST /invoices` pulls all `add_line_item` entries from the ticket into the invoice. Timer entries are NOT included.
|
||||
|
||||
**Note:** The `POST /invoices` response body does not include `line_items` — do `GET /invoices/{id}` to verify line items transferred correctly.
|
||||
|
||||
### Display formatting
|
||||
|
||||
When showing ticket lists, format as:
|
||||
|
||||
```
|
||||
#32164 New Jerry Burger Own cloud thing again
|
||||
#32163 New LeeAnn Parkinson Remote - Jim cant access his email
|
||||
#32162 Invoiced Len's Auto Brokerage Server upgrade
|
||||
```
|
||||
|
||||
When showing ticket detail, include:
|
||||
- Ticket number, subject, status, priority
|
||||
- Customer name + contact
|
||||
- Created date, due date, last updated
|
||||
- Assigned tech
|
||||
- Comments (most recent first, truncated to last 5)
|
||||
- Line items / billing status
|
||||
|
||||
### Billing workflow
|
||||
|
||||
**ALWAYS ask the user for minutes and labor type before logging any time. Never assume a default.**
|
||||
**ALWAYS show a preview of the comment + line items to the user before posting. Wait for confirmation.**
|
||||
**ALWAYS read `customer.prepay_hours` before choosing the labor product for emergency work.**
|
||||
|
||||
When `/syncro bill <number>` is called:
|
||||
1. `GET /tickets/{id}` for ticket detail, then `GET /customers/{customer_id}` to read `prepay_hours`
|
||||
2. Ask: "How many minutes should I bill, and what labor type? (remote / onsite / emergency / project / internal)"
|
||||
3. Decide product + quantity using the emergency-branching table above:
|
||||
- Non-prepaid + emergency → product `26184`, qty = actual hours
|
||||
- Prepaid + emergency → product `26118`, qty = actual hours × 1.5
|
||||
- Otherwise → per `--labor` mapping below, qty = actual hours
|
||||
4. `GET /products/{product_id}` → `.product.price_retail` to fetch the current rate
|
||||
5. Draft the comment body and show it to the user — with the product, quantity, rate, and computed total — for review before any POST
|
||||
6. Post comment: `POST /tickets/{id}/comment`
|
||||
7. Add billable line item: `POST /tickets/{id}/add_line_item` with `product_id`, `quantity`, `price_retail` (from step 4), `name`, `description`, `taxable: false`
|
||||
8. Create invoice: `POST /invoices` with `{"ticket_id": N, "customer_id": N, "category": "Standard"}`
|
||||
9. Verify invoice: `GET /invoices/{id}` → confirm `.invoice.total` matches `qty × price_retail`
|
||||
10. Update ticket status to `Invoiced`
|
||||
|
||||
**If `.invoice.total` comes back $0.00** (line items went in with null price): `PUT /tickets/{id}/update_line_item` with `price_retail` on each item, then `DELETE /invoices/{bad_id}` and re-POST `/invoices`. Recovery verified on #32203 (2026-04-23).
|
||||
|
||||
**Correct pattern:**
|
||||
```bash
|
||||
# Step 1: Post comment
|
||||
curl -s -X POST "${BASE}/tickets/${ID}/comment?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"subject": "Resolution", "body": "...", "hidden": false, "do_not_email": false}'
|
||||
|
||||
# Step 2: Add billable line item (convert minutes to decimal hours)
|
||||
# 60 min = 1.0, 30 min = 0.5, 45 min = 0.75, etc.
|
||||
# Always include price_retail — Syncro does NOT auto-apply rates via API
|
||||
curl -s -X POST "${BASE}/tickets/${ID}/add_line_item?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"product_id": 1190473, "quantity": 1.0, "price_retail": 150.00, "name": "Labor - Remote Business", "description": "...", "taxable": false}'
|
||||
|
||||
# Step 3: Create invoice
|
||||
curl -s -X POST "${BASE}/invoices?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"ticket_id": '"${ID}"', "customer_id": '"${CUST}"', "category": "Standard"}'
|
||||
|
||||
# Step 4: Verify line items transferred
|
||||
curl -s "${BASE}/invoices/${INVOICE_ID}?api_key=${API_KEY}" | jq '.invoice.line_items'
|
||||
|
||||
# Step 5: Mark ticket Invoiced
|
||||
curl -s -X PUT "${BASE}/tickets/${ID}?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"status": "Invoiced"}'
|
||||
```
|
||||
|
||||
`--labor` maps to product IDs: `remote` → 1190473, `onsite` → 26118, `emergency` → 26184, `project` → 9269129, `internal` → 9269124, `travel` → 26117, `website` → 68055
|
||||
|
||||
**Override:** `emergency` becomes `26118` with `quantity × 1.5` when the customer has `prepay_hours > 0`. See the Emergency billing branching table above.
|
||||
|
||||
### Error handling
|
||||
|
||||
- 401: API key invalid or expired
|
||||
- 404: ticket/customer/invoice not found
|
||||
- 422: validation error (show the error message from response body)
|
||||
- 429: rate limited (wait 60s and retry)
|
||||
|
||||
### Integration with session logs
|
||||
|
||||
When closing a ticket (`/syncro close`), offer to create a session log entry in `clients/<customer>/session-logs/` documenting what was resolved. Pull the ticket subject, comments, and resolution into a structured log.
|
||||
@@ -1,27 +1,43 @@
|
||||
# Memory Index
|
||||
|
||||
## Reference
|
||||
- [Community Forum (Flarum)](reference_community_forum.md) - Flarum forum at community.azcomputerguru.com, API access, database, posting workflow
|
||||
- [Radio Show Website](reference_radio_website.md) - Astro static site at radio.azcomputerguru.com on IX server
|
||||
- [IX Server SSH Access](reference_ix_server_ssh.md) - SSH access notes, no key auth from CachyOS workstation yet
|
||||
- [IX Access via Tailscale](reference_ix_access_tailscale.md) - IX server accessible with Tailscale on, no VPN needed
|
||||
- [Neptune Access via D2TESTNAS](reference_neptune_access_d2testnas.md) - Neptune must be routed through D2TESTNAS
|
||||
- [ACG-5070 Workstation](reference_workstation_setup.md) - Windows 11, replaced CachyOS. SOPS vault, Ollama, all dev tools.
|
||||
- [Matomo Analytics](reference_matomo_analytics.md) - Self-hosted analytics at analytics.azcomputerguru.com, site IDs, tracking for all 3 sites
|
||||
- [Dataforth Contact - AJ](reference_dataforth_contact.md) - AJ at Dataforth, dataforthgit@ email forwarding to him
|
||||
- [TickTick Integration](reference_ticktick_integration.md) - OAuth API integration, MCP server, SOPS vault creds, project/task CRUD
|
||||
|
||||
## Feedback
|
||||
- [D2TESTNAS SSH Access](feedback_d2testnas_ssh.md) - Use root@192.168.0.9 with Paper123!@#, not sysadmin
|
||||
- [Bypass Permissions Setting](feedback_bypass_permissions_setting.md) - Set permissions.defaultMode to bypassPermissions in settings.json on all machines
|
||||
- [365 Remediation Tool](feedback_365_remediation_tool.md) - Always means Graph API app fabb3421, not CIPP
|
||||
|
||||
## Machine
|
||||
- [ACG-5070 Workstation Setup](reference_workstation_setup.md) - Windows 11 Pro clean install 2026-03-30, replaced CachyOS. All tools installed.
|
||||
|
||||
## Project
|
||||
- [Audio Processor Architecture](project_audio_processor_architecture.md) - Segment-first pipeline: detect breaks before transcription for complete content capture
|
||||
- [Neptune Email Routing Issues](project_email_routing_neptune.md) - Multiple clients (devcon, Sorensen/rieussetcorp) have email not routing properly from Neptune
|
||||
- [Neptune SBR Email Routing Setup](project_neptune_sbr_email_routing.md) - Full SBR routing chain, config file locations, MailProtector integration, access methods
|
||||
- [Dataforth Test Datasheet Pipeline](project_datasheet_pipeline.md) - Full pipeline rebuilt 2026-03-27. Server-side generation replaces DFWDS/Uploader. Website upload still broken.
|
||||
- [Dataforth Security Incident](project_dataforth_incident_2026-03-27.md) - DF-JOEL2 compromised, MFA deployed, IC3 filed. CA policies enforce April 4.
|
||||
# Memory Index
|
||||
|
||||
## Reference
|
||||
- [Community Forum (Flarum)](reference_community_forum.md) - Flarum forum at community.azcomputerguru.com, API access, database, posting workflow
|
||||
- [Radio Show Website](reference_radio_website.md) - Astro static site at radio.azcomputerguru.com on IX server
|
||||
- [IX Server SSH Access](reference_ix_server_ssh.md) - SSH access notes, no key auth from CachyOS workstation yet
|
||||
- [IX Access via Tailscale](reference_ix_access_tailscale.md) - IX server accessible with Tailscale on, no VPN needed
|
||||
- [Neptune Access via D2TESTNAS](reference_neptune_access_d2testnas.md) - Neptune must be routed through D2TESTNAS
|
||||
- [ACG-5070 Workstation](reference_workstation_setup.md) - Windows 11, replaced CachyOS. SOPS vault, Ollama, all dev tools.
|
||||
- [Matomo Analytics](reference_matomo_analytics.md) - Self-hosted analytics at analytics.azcomputerguru.com, site IDs, tracking for all 3 sites
|
||||
- [Dataforth Contact - AJ](reference_dataforth_contact.md) - AJ at Dataforth, dataforthgit@ email forwarding to him
|
||||
- [TickTick Integration](reference_ticktick_integration.md) - OAuth API integration, MCP server, SOPS vault creds, project/task CRUD
|
||||
- [Client Docs Structure](reference_client_docs_structure.md) - clients/<name>/docs/ layout (overview, network, servers, cloud, security, rmm, issues). Template at clients/_client_template/.
|
||||
- [MSP Audit Scripts](reference_msp_audit_scripts.md) - server_audit.ps1 / workstation_audit.ps1 at projects/msp-tools/msp-audit-scripts/. ScreenConnect 80-char rule.
|
||||
- [GuruRMM Server Layout](reference_gururmm_server.md) - SSH as `guru`, repo at /home/guru/gururmm, deploy to /var/www/gururmm/dashboard/
|
||||
- [GuruRMM API — run script on agent](reference_gururmm_api.md) - POST /api/agents/:id/command with command_type=powershell + command text; poll /api/commands/:id for stdout/stderr. Use instead of ScreenConnect copy-paste.
|
||||
- [Pluto Build Server](reference_pluto_build_server.md) - General-purpose Windows build VM, 172.16.3.36, SSH as Administrator, MSVC toolchain — use for any EXE (utilities, Howard's tools, GuruRMM agent)
|
||||
|
||||
## Users
|
||||
- [Howard Enos](user_howard.md) — Mike's brother, technician, full trust/access. Known machine: ACG-TECH03L.
|
||||
|
||||
## Feedback
|
||||
- [D2TESTNAS SSH Access](feedback_d2testnas_ssh.md) - Use root@192.168.0.9 with Paper123!@#, not sysadmin
|
||||
- [Bypass Permissions Setting](feedback_bypass_permissions_setting.md) - Set permissions.defaultMode to bypassPermissions in settings.json on all machines
|
||||
- [365 Remediation Tool](feedback_365_remediation_tool.md) - Always means Graph API app fabb3421, not CIPP
|
||||
- [Ollama Tier-0 Routing](feedback_ollama_tier0_routing.md) - Route drafts/summaries/classifications through Ollama (qwen3:14b). Mike designed ClaudeTools this way — not optional.
|
||||
- [Syncro Emergency Billing](feedback_syncro_emergency_billing.md) — Emergency = 1.5× multiplier, not additive. Branch by `customer.prepay_hours`: no-prepaid → `26184` at actual hrs; prepaid → `26118` at hrs×1.5. Never stack. Always set `price_retail`.
|
||||
- [Identity precedence](feedback_identity_precedence.md) — Trust `.claude/identity.json` over the system-reminder `userEmail` hint when they disagree (shared-login machines).
|
||||
|
||||
## Machine
|
||||
- [ACG-5070 Workstation Setup](reference_workstation_setup.md) - Windows 11 Pro clean install 2026-03-30, replaced CachyOS. All tools installed.
|
||||
|
||||
## Pending Setup
|
||||
- [Mac gururmm setup pending](project_mac_gururmm_setup_pending.md) — ACTION REQUIRED: run `bash scripts/install-hooks.sh` in gururmm repo on Mikes-MacBook-Air before any RMM work
|
||||
|
||||
## Project
|
||||
- [Sync script bug — untracked files](project_sync_script_bug.md) — Flagged for Mike. `.claude/scripts/sync.sh` line 53 misses untracked-only changes; one-line fix included.
|
||||
- [MasterBooter Side Project](project_masterbooter.md) — Howard's Rust+Slint Windows deployment toolkit at C:\MasterBooter, separate from client work. Do not log to clients/.
|
||||
- [Audio Processor Architecture](project_audio_processor_architecture.md) - Segment-first pipeline: detect breaks before transcription for complete content capture
|
||||
- [Neptune Email Routing Issues](project_email_routing_neptune.md) - Multiple clients (devcon, Sorensen/rieussetcorp) have email not routing properly from Neptune
|
||||
- [Neptune SBR Email Routing Setup](project_neptune_sbr_email_routing.md) - Full SBR routing chain, config file locations, MailProtector integration, access methods
|
||||
- [Dataforth Test Datasheet Pipeline](project_datasheet_pipeline.md) - Full pipeline rebuilt 2026-03-27. Server-side generation replaces DFWDS/Uploader. Website upload still broken.
|
||||
- [Dataforth Security Incident](project_dataforth_incident_2026-03-27.md) - DF-JOEL2 compromised, MFA deployed, IC3 filed. CA policies enforce April 4.
|
||||
|
||||
@@ -10,6 +10,8 @@ When user says "365 remediation tool" or "remediation tool", they ALWAYS mean th
|
||||
|
||||
**How to apply:** Authenticate directly via Graph API using the app's client secret from SOPS vault (`msp-tools/claude-msp-access-graph-api.sops.yaml`), get tenant ID from OpenID discovery for the target domain, and query Graph API endpoints directly. No browser/UI needed.
|
||||
|
||||
**Preferred invocation: use the `/remediation-tool` skill** (`.claude/skills/remediation-tool/`, also surfaces as a `/remediation-tool` command). It wraps tenant resolution, token caching, the 10-point user breach check, and tenant-wide sweep. Remediation actions are gated behind explicit `YES` confirmation. Reference docs at `references/gotchas.md`, `references/graph-endpoints.md`, `references/checklist.md`.
|
||||
|
||||
### Directory Role Requirements (discovered 2026-04-01)
|
||||
|
||||
Graph API permissions alone are NOT sufficient for privileged operations. The service principal also needs Entra directory roles assigned per-tenant:
|
||||
|
||||
11
.claude/memory/feedback_identity_precedence.md
Normal file
11
.claude/memory/feedback_identity_precedence.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: identity.json beats userEmail hint
|
||||
description: When .claude/identity.json and system-reminder userEmail disagree, trust identity.json — it's the per-machine source of truth
|
||||
type: feedback
|
||||
---
|
||||
|
||||
When the system-reminder context claims `userEmail = mike@azcomputerguru.com` but `.claude/identity.json` says `howard`, trust identity.json. The userEmail comes from global Claude Code config (Mike set up the login on both machines under his account); identity.json is the per-machine, gitignored file that records who actually sits at this keyboard.
|
||||
|
||||
**Why:** On 2026-04-23 I addressed Howard as "Mike" because the claudeMd/userEmail context said so. Howard corrected me. The CLAUDE.md onboarding flow explicitly defines identity.json as the authoritative local identity.
|
||||
|
||||
**How to apply:** At every session start, read `.claude/identity.json` FIRST (as CLAUDE.md step 1 requires) and greet from that file's `full_name`. Ignore the `# userEmail` context block for greeting purposes. If identity.json is missing, follow the first-machine bootstrap flow in CLAUDE.md — don't fall back to userEmail.
|
||||
46
.claude/memory/feedback_ollama_tier0_routing.md
Normal file
46
.claude/memory/feedback_ollama_tier0_routing.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: Route Tier-0 tasks through Ollama (Mike's ClaudeTools design intent)
|
||||
description: Drafts, summaries, classifications, extractions MUST go through Ollama per Mike's tiered-model architecture. Don't default to Claude inference for low-stakes text generation.
|
||||
type: feedback
|
||||
---
|
||||
|
||||
Route Tier-0 tasks (summaries, classifications, drafts, extractions) through Ollama. Not optional — this is how Mike designed ClaudeTools to work.
|
||||
|
||||
**Why:** Mike built the tiered-model architecture (`CLAUDE.md` Model Routing section + `.claude/OLLAMA.md`) deliberately. Tier 0 is free + fast + private. Defaulting to Claude for every drafting task burns context window and Anthropic tokens on work that qwen3:14b does fine.
|
||||
|
||||
**How to apply:**
|
||||
- Drafting emails, session-log paragraphs, status-update sentences, commit-message first-drafts → qwen3:14b
|
||||
- Summarizing long output (Graph JSON, PowerShell transcripts, log tails) → qwen3:14b
|
||||
- Extracting structured data from text → qwen3:14b
|
||||
- Suggesting refactors / generating docstrings → codestral:22b (then review)
|
||||
- NEVER for: auth decisions, credential handling, production migrations, security review, citation work, production-change scripts
|
||||
|
||||
**Endpoint resolution (updated 2026-04-22 in `.claude/OLLAMA.md`):**
|
||||
```bash
|
||||
if curl -s -m 2 http://localhost:11434/api/tags >/dev/null 2>&1; then
|
||||
OLLAMA="http://localhost:11434"
|
||||
else
|
||||
OLLAMA="http://100.92.127.64:11434"
|
||||
fi
|
||||
```
|
||||
|
||||
HOWARD-HOME has the canonical models loaded locally (qwen3:14b, codestral:22b, nomic-embed-text, plus bonus qwen3-coder:30b) — so HOWARD-HOME uses local Ollama, not Mike's. Zero Tailscale hop.
|
||||
|
||||
**Call pattern for qwen3 — use `/api/chat` with `think:false`**, NOT `/api/generate`. qwen3 on generate endpoint dumps reasoning into internal thinking tokens and returns empty `response` field. Chat endpoint with `think:false` returns clean content in `message.content`:
|
||||
|
||||
```python
|
||||
body = json.dumps({
|
||||
'model':'qwen3:14b',
|
||||
'messages':[{'role':'user','content': prompt}],
|
||||
'stream':False,
|
||||
'think':False
|
||||
}).encode()
|
||||
# POST to OLLAMA + '/api/chat'
|
||||
# Read res['message']['content']
|
||||
```
|
||||
|
||||
Codestral doesn't need `think:false` — just use it on `/api/chat` normally.
|
||||
|
||||
Cold-start ~30-50s on first call per model per session; warm calls 1-5s.
|
||||
|
||||
**Incident 2026-04-22:** Spent an entire Cascades rollout session (G1 hygiene, orphan cleanup, risk register, synology discovery, etc.) without routing a single task through Ollama despite many drafting opportunities (report drafts, summary text, email drafts). Howard called this out: "just make sure ollama is being used as mike has designed claudetools to work."
|
||||
23
.claude/memory/feedback_syncro_emergency_billing.md
Normal file
23
.claude/memory/feedback_syncro_emergency_billing.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Syncro emergency/after-hours billing — check prepay_hours first
|
||||
description: Emergency labor is 1.5× multiplier, not additive. Branch by customer.prepay_hours — wrong branch doubles or undercharges. Applies to every /syncro bill for emergency work.
|
||||
type: feedback
|
||||
---
|
||||
|
||||
**Rule:** Before adding any Emergency/after-hours labor line item on a Syncro ticket, `GET /customers/<id>` and read `prepay_hours`.
|
||||
|
||||
- If `prepay_hours == 0` (no prepaid block): use product `26184` (Labor - Emergency/After Hours) at quantity = actual hours. The $262.50/hr rate already has the 1.5× multiplier baked in.
|
||||
- If `prepay_hours > 0` (customer has a prepaid block): use product `26118` (Labor - Onsite) at quantity = actual hours × 1.5. Prepaid blocks debit by QUANTITY, not dollars, so we bump qty instead of swapping to the Emergency product.
|
||||
|
||||
Never stack `26118` + `26184` for the same hour of work. Pick one path based on the prepaid state.
|
||||
|
||||
**Why:** Learned on ticket #32203 (Desert Auto Tech) 2026-04-23. Howard asked to bill "1 hour onsite + 1 hour emergency onsite." I posted both as separate additive line items and the invoice came out at $437.50 when the correct bill for 1 actual hour of emergency work was $262.50. Winter caught it and explained the rule: "the goal is to have it bill at time and a half." The Emergency product = time-and-a-half by rate; prepaid accounts = time-and-a-half by quantity. Swapping products AND multiplying quantity double-counts.
|
||||
|
||||
**How to apply:**
|
||||
- Every `/syncro bill` for emergency/after-hours work: check `prepay_hours` BEFORE choosing the product. Do not shortcut this.
|
||||
- For a 2-hour emergency job:
|
||||
- Non-prepaid customer → one line, 2.0 hrs × `26184` → $525.00
|
||||
- Prepaid customer → one line, 3.0 hrs × `26118` → 3 hours debit from block
|
||||
- Always set `price_retail` explicitly on `add_line_item`. The old "omit and let Syncro auto-calc" guidance was wrong — the rate does not populate from the product config, and the invoice will post at $0 if `price_retail` is missing. Fetch the current rate with `GET /products/<id>`.
|
||||
- Never let a customer-facing invoice post without verifying `.invoice.total` matches the expected `qty × price_retail`.
|
||||
- Full rules and examples live in `.claude/commands/syncro.md` under the "Labor product IDs" section.
|
||||
26
.claude/memory/project_mac_gururmm_setup_pending.md
Normal file
26
.claude/memory/project_mac_gururmm_setup_pending.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: Mac gururmm hook setup pending
|
||||
description: Mikes-MacBook-Air needs install-hooks.sh run in gururmm repo — one-time setup to prevent sqlx migration drift
|
||||
type: project
|
||||
---
|
||||
|
||||
The gururmm repo on this Mac needs a one-time setup step that was applied to all other machines on 2026-04-19.
|
||||
|
||||
**Action required (do this now, before any gururmm work):**
|
||||
|
||||
```bash
|
||||
cd /Users/azcomputerguru/ClaudeTools/projects/msp-tools/guru-rmm
|
||||
git pull
|
||||
bash scripts/install-hooks.sh
|
||||
```
|
||||
|
||||
**What this does:**
|
||||
- Sets `core.hooksPath = scripts/hooks/` so the pre-commit CRLF check is active
|
||||
- Sets `core.autocrlf=false` and `core.eol=lf` locally and globally
|
||||
- Prevents sqlx migration checksum drift (root cause: CRLF vs LF sha384 mismatch)
|
||||
|
||||
**Why:** The gururmm build server refused to start after a rebuild because migration file hashes differed between what was stored in `_sqlx_migrations` and the current files. Root cause was CRLF line endings from Windows commits. Fixed with `.gitattributes` + per-machine git config. This command applies the git config side.
|
||||
|
||||
macOS defaults to LF, so this is low-risk — mainly sets the hooksPath so the pre-commit guard is active.
|
||||
|
||||
**After running:** Delete this memory file or mark it resolved.
|
||||
31
.claude/memory/project_masterbooter.md
Normal file
31
.claude/memory/project_masterbooter.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: MasterBooter Side Project
|
||||
description: Howard's personal side project at C:\MasterBooter — Windows deployment toolkit, separate from client/MSP work. Do not mix with clients/ content.
|
||||
type: project
|
||||
---
|
||||
|
||||
MasterBooter is Howard's personal Rust + Slint Windows deployment toolkit at `C:\MasterBooter`. Single-portable-EXE targeting IT/MSP/repair-shop techs. Four modes: Backup/Restore, Windows Deploy, WinPE Builder (WinRE-based), System Prep. Public GitHub repo: `Howweird/Masterbooter`.
|
||||
|
||||
**Why:** Side project separate from Arizona Computer Guru client work. Howard is learning Rust through it — code is heavily commented by design. Not a commercial product yet, no paying customers.
|
||||
|
||||
**How to apply:**
|
||||
- When Howard mentions MasterBooter, WinPE builder, winpe.rs, deploy.rs, etc., context is `C:\MasterBooter`, NOT the `clients/` folder in ClaudeTools.
|
||||
- Don't log MasterBooter sessions to `clients/` — they are personal project work, not customer engagements.
|
||||
- Project has its own `C:\MasterBooter\CLAUDE.md` with its own rules. Follow those when in that directory.
|
||||
- Heavy comments are intentional (learning), not tech debt.
|
||||
|
||||
**Key docs in C:\MasterBooter:**
|
||||
- `VISION.md` — goals, 4 modes
|
||||
- `REQUIREMENTS.md` — feature tracking (sections 1-10 complete, Section 11 added 2026-04-17 with F1-F25 planned)
|
||||
- `DECISIONS.md` — ADRs (ADR-001 through ADR-014, Rust/Slint switch is ADR-005)
|
||||
- `EXPANSION_PLAN.md` — full roadmap added 2026-04-17, phased execution plan
|
||||
- `TODO_CLEANUP.md` — refactor backlog from March 2026 code review
|
||||
- `CHANGELOG.md` — actively maintained
|
||||
|
||||
**Current status (2026-04-17):** v0.2.1 released. Phase 1 reliability work starting — logging, tempfile, DISM /English, quick-xml, tests, CLI, GHA. Then 26 new features (F1-F25) across 4 tiers + tool swaps.
|
||||
|
||||
**Reference programs Howard studies for ideas** (all in `C:\Users\howar\ClaudeSourceFiles\` or `C:\`):
|
||||
- AMPIPIT (C:\AMPIPIT) — primary Rust+Slint reference
|
||||
- GhostWin — Rust WIM/deploy reference
|
||||
- d7x (C:\Users\howar\ClaudeSourceFiles\d7x) — MSP tool catalog inspiration (55+ bundled tools)
|
||||
- Windows Setup Helper, Unattend Generator, SysprepPreparator, PhoenixPE
|
||||
23
.claude/memory/project_sync_script_bug.md
Normal file
23
.claude/memory/project_sync_script_bug.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Sync script bug — untracked files
|
||||
description: Flagged for Mike — .claude/scripts/sync.sh misses untracked-only changes
|
||||
type: project
|
||||
---
|
||||
|
||||
`.claude/scripts/sync.sh` line 53 uses `git diff-index --quiet HEAD --` to detect local changes. This only flags **tracked** files with modifications. Brand-new untracked files (a new report, new session log, new memory) will NOT be detected on their own — they only get swept up when a tracked file is also dirty (because `git add -A` then runs).
|
||||
|
||||
Symptom seen 2026-04-17 by Howard: added a single new report file, ran /sync, script said "No local changes to commit" and did nothing. Workaround was `git add <file>` first, then re-run.
|
||||
|
||||
**Why:** `git diff-index` ignores untracked files by design. Needs `git status --porcelain` (any output = changes) or equivalent.
|
||||
|
||||
**How to apply:** Mike — small one-line fix in `.claude/scripts/sync.sh`. Suggested replacement:
|
||||
|
||||
```bash
|
||||
# Before (line 53):
|
||||
if ! git diff-index --quiet HEAD -- 2>/dev/null; then
|
||||
|
||||
# After:
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
```
|
||||
|
||||
Also applies to the Sync Summary's `git diff --stat $LOCAL_BEFORE..HEAD` — may need review to make sure the summary range still makes sense after the detection fix.
|
||||
33
.claude/memory/reference_client_docs_structure.md
Normal file
33
.claude/memory/reference_client_docs_structure.md
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Client Documentation Structure
|
||||
description: Howard's MSP client docs live under clients/<name>/docs/ with a standard subfolder layout (overview, network, servers, cloud, security, rmm, issues). Template at clients/_client_template/.
|
||||
type: reference
|
||||
---
|
||||
|
||||
Each active client has structured Markdown documentation under `clients/<client-name>/docs/`:
|
||||
|
||||
| File / Folder | Purpose |
|
||||
|---|---|
|
||||
| `overview.md` | Company info, contacts, environment summary, device counts |
|
||||
| `network/topology.md` | Switches, APs, cabling, interconnects |
|
||||
| `network/vlans.md` | VLAN table, subnets, inter-VLAN routing |
|
||||
| `network/dns.md` | DNS servers, zones, records, forwarders |
|
||||
| `network/dhcp.md` | Scopes, reservations, relay config |
|
||||
| `network/firewall.md` | Rules, NAT, VPN, interfaces |
|
||||
| `network/wifi.md` | SSIDs, security, AP assignments |
|
||||
| `servers/<name>.md` | Per-server docs (use `server_template.md`) |
|
||||
| `cloud/m365.md` | Tenant, licensing, Exchange, Entra ID |
|
||||
| `cloud/azure.md` | Subscriptions, VMs, networking |
|
||||
| `security/antivirus.md` | EDR/AV product, deployment status |
|
||||
| `security/backup.md` | Backup jobs, targets, DR plan |
|
||||
| `rmm/rmm.md` | RMM product, agent counts, patch policy |
|
||||
| `issues/log.md` | Historical incident log with root causes |
|
||||
| `billing-log.md` | Per-client billing / work log |
|
||||
|
||||
Clients currently documented (imported 2026-04-16 from Howard's `C:\Users\howar\Clients`):
|
||||
anaise, cascades-tucson, dataforth, instrumental-music-center, khalsa, kittle, lens-auto-brokerage.
|
||||
|
||||
Credentials NEVER go inline in these docs — reference SOPS vault instead:
|
||||
`clients/<name>/<system>.sops.yaml` field path.
|
||||
|
||||
The template at `clients/_client_template/` is the scaffold for new clients.
|
||||
92
.claude/memory/reference_gururmm_api.md
Normal file
92
.claude/memory/reference_gururmm_api.md
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
name: GuruRMM API — run PowerShell on any agent
|
||||
description: API endpoints, auth flow, and curl recipe to execute a script on any GuruRMM agent and retrieve output. Use this instead of asking user to paste script into ScreenConnect.
|
||||
type: reference
|
||||
---
|
||||
|
||||
# GuruRMM API — Execute Script on an Agent
|
||||
|
||||
**API base:** `http://172.16.3.30:3001` (reachable from HOWARD-HOME and similar dev machines via Tailscale — not reachable from cascades internal-network-only boxes, but that doesn't matter since the API talks to the agent, not the target machine).
|
||||
|
||||
**Auth creds:** `infrastructure/gururmm-server.sops.yaml` → `credentials.gururmm-api.admin-email` + `admin-password`. Login returns a JWT valid for ~24h (expires 86400s from iat).
|
||||
|
||||
## Flow
|
||||
|
||||
```bash
|
||||
VAULT="$PWD/.claude/scripts/vault.sh"
|
||||
EMAIL=$(bash "$VAULT" get-field infrastructure/gururmm-server.sops.yaml credentials.gururmm-api.admin-email)
|
||||
PASS=$(bash "$VAULT" get-field infrastructure/gururmm-server.sops.yaml credentials.gururmm-api.admin-password)
|
||||
|
||||
JWT=$(curl -s -X POST http://172.16.3.30:3001/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"email\":\"$EMAIL\",\"password\":\"$PASS\"}" \
|
||||
| python -c "import json,sys; print(json.load(sys.stdin)['token'])")
|
||||
|
||||
# List agents (find the agent_id for the host you want)
|
||||
curl -s http://172.16.3.30:3001/api/agents -H "Authorization: Bearer $JWT"
|
||||
|
||||
# Submit a PowerShell command — works with any file, json-encode to preserve quotes/newlines
|
||||
AGENT="<agent-uuid>"
|
||||
PAYLOAD=$(python -c "
|
||||
import json
|
||||
with open('path/to/script.ps1','r',encoding='utf-8') as f: s=f.read()
|
||||
print(json.dumps({'command_type':'powershell','command':s}))
|
||||
")
|
||||
RESP=$(curl -s -X POST http://172.16.3.30:3001/api/agents/$AGENT/command \
|
||||
-H "Authorization: Bearer $JWT" -H "Content-Type: application/json" -d "$PAYLOAD")
|
||||
CMD_ID=$(echo "$RESP" | python -c "import json,sys; print(json.load(sys.stdin)['command_id'])")
|
||||
|
||||
# Poll until completed (status values: running, completed, failed, timeout)
|
||||
while true; do
|
||||
STATUS=$(curl -s http://172.16.3.30:3001/api/commands/$CMD_ID -H "Authorization: Bearer $JWT" \
|
||||
| python -c "import json,sys; print(json.load(sys.stdin)['status'])")
|
||||
[ "$STATUS" != "running" ] && break
|
||||
sleep 5
|
||||
done
|
||||
|
||||
# Fetch result (stdout / stderr / exit_code)
|
||||
curl -s http://172.16.3.30:3001/api/commands/$CMD_ID -H "Authorization: Bearer $JWT"
|
||||
```
|
||||
|
||||
## Required request fields
|
||||
|
||||
`POST /api/agents/:id/command` requires:
|
||||
- `command_type` — the interpreter. Valid values include `powershell`, `shell`, `script`, `exec` — any string is accepted by the API but the Windows agent only runs powershell-compatible content. Use `powershell` for Windows agents.
|
||||
- `command` — the script text. JSON-encode to preserve newlines, quotes, and dollar-sign escapes.
|
||||
|
||||
## Response shape (from `/api/commands/:cmd_id`)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"agent_id": "uuid",
|
||||
"command_type": "powershell",
|
||||
"command_text": "...",
|
||||
"status": "completed", // or running | failed | timeout
|
||||
"exit_code": 0,
|
||||
"stdout": "...",
|
||||
"stderr": "...",
|
||||
"created_at": "ISO-8601",
|
||||
"started_at": "ISO-8601",
|
||||
"completed_at": "ISO-8601"
|
||||
}
|
||||
```
|
||||
|
||||
## When to use this
|
||||
|
||||
- Readiness / diagnostic checks on any client server where GuruRMM is installed
|
||||
- One-off remediation without needing ScreenConnect copy-paste
|
||||
- Anywhere you'd otherwise ask the user to paste a script manually
|
||||
|
||||
## When NOT to use this
|
||||
|
||||
- When the agent isn't enrolled in GuruRMM (check `GET /api/agents` first)
|
||||
- For interactive sessions (no stdin; single-shot execution)
|
||||
- For >1 MB of script (untested — keep scripts modular)
|
||||
|
||||
## Notes
|
||||
- Script output is limited; if you need large output, have the script write to a file on the agent and fetch via a separate command
|
||||
- `command_type: "powershell"` runs in the SYSTEM context on Windows (agent runs as LocalSystem)
|
||||
- Idempotent commands only — there is no transactional rollback
|
||||
- The tunnel API (`/api/v1/tunnel/...`) is a planned interactive feature per `.claude/gururmm-tunnel-plan.md`, not yet deployed as of 2026-04-22. Stick to `/api/agents/:id/command` for now.
|
||||
- Agents enrolled as of 2026-04-22 include CS-SERVER (`6766e973-e703-47c1-be56-76950290f87c`) for Cascades, DESKTOP-DLTAGOI for Cascades LE, AD2 for AZ Computer Guru. Use `GET /api/agents` for the live list.
|
||||
14
.claude/memory/reference_gururmm_server.md
Normal file
14
.claude/memory/reference_gururmm_server.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: GuruRMM Server Layout
|
||||
description: SSH user, home directory, and deploy paths on 172.16.3.30
|
||||
type: reference
|
||||
---
|
||||
|
||||
SSH user is `guru`, NOT `mike`. Home directory is `/home/guru/`.
|
||||
|
||||
- Repo: `/home/guru/gururmm`
|
||||
- Dashboard build: `cd /home/guru/gururmm/dashboard && npm run build`
|
||||
- Deploy: `sudo cp -r dist/* /var/www/gururmm/dashboard/`
|
||||
- Other dirs under `/home/guru/`: `guru-connect`, `guruconnect-server`, `backups`
|
||||
|
||||
**Why:** First SSH session assumed `/home/mike/` — does not exist. Only users with home dirs are `guru` and `gitea-runner`.
|
||||
29
.claude/memory/reference_msp_audit_scripts.md
Normal file
29
.claude/memory/reference_msp_audit_scripts.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: MSP Audit Scripts
|
||||
description: server_audit.ps1 and workstation_audit.ps1 for on-demand auditing via ScreenConnect Toolbox. Also hosted on GitHub (Howweird/msp-audit-scripts) for remote fetch.
|
||||
type: reference
|
||||
---
|
||||
|
||||
Location in claudetools: `projects/msp-tools/msp-audit-scripts/`.
|
||||
|
||||
Scripts:
|
||||
- `server_audit.ps1` — Full server + AD + security audit, outputs JSON to `C:\Temp\`.
|
||||
- `workstation_audit.ps1` — Full workstation audit, outputs JSON to `C:\Temp\`.
|
||||
- `README.md` — Usage notes.
|
||||
|
||||
Remote fetch URL pattern (for ScreenConnect Toolbox):
|
||||
```
|
||||
https://raw.githubusercontent.com/Howweird/msp-audit-scripts/master/server_audit.ps1
|
||||
https://raw.githubusercontent.com/Howweird/msp-audit-scripts/master/workstation_audit.ps1
|
||||
```
|
||||
|
||||
ScreenConnect Toolbox PowerShell rules (IMPORTANT):
|
||||
- No line may exceed 80 chars — Toolbox silently truncates long lines
|
||||
- Store long URLs/paths in variables first
|
||||
- Use multi-line try/catch blocks, never single-line
|
||||
- Paste whole scripts as one command — no inline comments between blocks
|
||||
|
||||
Utility scripts also at `projects/msp-tools/utilities/`:
|
||||
- `clean_printer_ports.ps1`
|
||||
- `win11_upgrade.ps1`
|
||||
- `screenconnect-toolbox-commands.txt` (saved Toolbox one-liners)
|
||||
56
.claude/memory/reference_pluto_build_server.md
Normal file
56
.claude/memory/reference_pluto_build_server.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: Pluto Build Server
|
||||
description: General-purpose Windows build VM on Jupiter — for any EXE needing native Windows compilation (utilities, Howard's tools, GuruRMM agent, etc.)
|
||||
type: reference
|
||||
---
|
||||
|
||||
Pluto is a Windows Server VM on Jupiter. It is the **general-purpose Windows build machine** for any project needing a native Windows executable — not just GuruRMM.
|
||||
|
||||
- **Hostname:** PLUTO (VM on Jupiter)
|
||||
- **Static IP:** 172.16.3.36 (confirmed static 2026-04-19)
|
||||
- **SSH:** `ssh -i ~/.ssh/id_ed25519 Administrator@172.16.3.36` (key auth)
|
||||
- **Authorized key:** `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINXR2BOcFAlOPuB7OYOKfOZDNd3u1tCt/IINRH9beFyB guru@DESKTOP-0O8A1RL`
|
||||
|
||||
## Installed Toolchain
|
||||
|
||||
- **Rust:** stable-x86_64-pc-windows-msvc (rustup at `C:\Users\Administrator\.cargo\bin`)
|
||||
- **VS Build Tools:** Installed with `Microsoft.VisualStudio.Workload.VCTools` (MSVC linker, CRT, Windows SDK)
|
||||
- **Git:** v2.47.1.windows.2
|
||||
- **OpenSSH:** Win32-OpenSSH, sshd set to Automatic startup
|
||||
|
||||
## Use Cases
|
||||
|
||||
Use Pluto when you need a **native Windows MSVC build** — produces proper `.exe` files with no MinGW runtime dependency. Examples:
|
||||
- Utilities (internal tooling, one-off scripts compiled to EXE)
|
||||
- Howard's tech tools (MasterBooter, Slint GUI apps, etc.)
|
||||
- GuruRMM agent MSVC builds (when MSVC target is preferred over the automated MinGW build on the Linux server)
|
||||
- Anything using Windows-only APIs or needing code signing via signtool
|
||||
|
||||
**Note:** Routine GuruRMM agent builds are automated on the Linux server (172.16.3.30) via MinGW + jsign. Use Pluto for MSVC-specific builds or one-off tooling.
|
||||
|
||||
## Directory Layout
|
||||
|
||||
- `C:\builds\` — general project builds (create a subdirectory per project)
|
||||
- `C:\gururmm\` — GuruRMM repo clone
|
||||
|
||||
## Typical Build Workflow
|
||||
|
||||
```bash
|
||||
# 1. SSH in
|
||||
ssh -i ~/.ssh/id_ed25519 Administrator@172.16.3.36
|
||||
|
||||
# 2. Clone or pull project
|
||||
git clone https://azcomputerguru:<token>@git.azcomputerguru.com/azcomputerguru/<repo>.git C:\builds\<project>
|
||||
|
||||
# 3. Build
|
||||
cd C:\builds\<project>
|
||||
cargo build --release
|
||||
|
||||
# 4. SCP output back
|
||||
# From workstation:
|
||||
scp -i ~/.ssh/id_ed25519 Administrator@172.16.3.36:"C:/builds/<project>/target/release/<name>.exe" ./
|
||||
```
|
||||
|
||||
## Not Neptune
|
||||
|
||||
Neptune is a separate existing server (email/web hosting). Pluto is only for builds.
|
||||
13
.claude/memory/user_howard.md
Normal file
13
.claude/memory/user_howard.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
name: Howard Enos — team member
|
||||
description: Howard is Mike's brother and employee at AZ Computer Guru. Technician role with full trust and full access. Uses claudetools for MSP tracking and daily client work.
|
||||
type: user
|
||||
---
|
||||
|
||||
Howard Enos is a technician at Arizona Computer Guru LLC and Mike Swanson's brother. He has full access to all systems, credentials, and client data — same level as Mike. No permission gating.
|
||||
|
||||
Known machine: ACG-TECH03L (laptop). Desktop hostname TBD (will be registered on first sync).
|
||||
|
||||
When working with Howard, treat him exactly as you would Mike — same context loading, same credential access, same capabilities. He uses claudetools for MSP work tracking, client management, and daily IT operations.
|
||||
|
||||
His git commits should show `Howard Enos <howard@azcomputerguru.com>`.
|
||||
7
.claude/messages/for-howard.md
Normal file
7
.claude/messages/for-howard.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Messages for Howard
|
||||
|
||||
Check this file at sync. Delete items after you've addressed them.
|
||||
|
||||
---
|
||||
|
||||
_No active messages._
|
||||
80
.claude/messages/for-mike.md
Normal file
80
.claude/messages/for-mike.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Note for Mike
|
||||
|
||||
Check this file at sync. Delete items after you've addressed them.
|
||||
|
||||
---
|
||||
|
||||
## From Howard, 2026-04-22 — Per-user Syncro keys (attribution fix)
|
||||
|
||||
I hit the issue that my Syncro comments/line items on ticket #32179 were getting logged as you (user_id 1735) because we share your API key. Fixed it with per-user tokens:
|
||||
|
||||
- Generated my own Syncro API token (Custom, admin, indefinite) → `user_id 1750`
|
||||
- Added vault entry: `msp-tools/syncro-howard.sops.yaml`
|
||||
- Patched `.claude/commands/syncro.md` to pick the key from `identity.json`'s `user` field, falls back to the shared `msp-tools/syncro.sops.yaml` if no per-user file exists
|
||||
- Verified `/me` now returns Howard Enos on my machine
|
||||
|
||||
**When you get a chance** (after Valleywide settles), do the same for yourself so the shared key can be retired:
|
||||
|
||||
1. Syncro → Admin → API Tokens → New (integration or custom, full scopes)
|
||||
2. `cat > $VAULT_ROOT/msp-tools/syncro-mike.sops.yaml <<YAML ... YAML` (template in the patched syncro.md)
|
||||
3. `cd $VAULT_ROOT && sops --encrypt --in-place msp-tools/syncro-mike.sops.yaml`
|
||||
4. Commit + push vault. The skill will pick it up automatically on your next sync.
|
||||
|
||||
After your key is in place we can delete `msp-tools/syncro.sops.yaml` (shared). Until then the skill warns on stderr when it falls back to the shared key.
|
||||
|
||||
---
|
||||
|
||||
## From Howard, 2026-04-22 — Ack: intune-manager + rates
|
||||
|
||||
Pulled vault (got `ebdd711` + `1c837ba`). intune-manager vault file loads fine now. Tried a token against grabblaw.com — returns `AADSTS700016` (app not consented in that tenant). Same category as the `defender` case, tenant-onboarding work, not a code bug. No action needed from you.
|
||||
|
||||
Rates reply on Syncro — understood, will omit `price_retail` going forward. Saw the syncro.md update.
|
||||
|
||||
Good luck with Valleywide — saw the NVRAM corruption log. Holler if you need a hand with anything from here.
|
||||
|
||||
---
|
||||
|
||||
## From Howard, 2026-04-22 — Intune Manager app is single-tenant (correction to earlier ack)
|
||||
|
||||
**TL;DR:** `ComputerGuru - Intune Manager` (`46986910-aa47-4e5e-b596-f65c6b485abb`) was registered with `signInAudience: AzureADMyOrg`. No external tenant can consent it. Needs a one-field PATCH to `AzureADMultipleOrgs`. Every other MSP app is already multi-tenant.
|
||||
|
||||
**Evidence** (pulled today via Management app):
|
||||
|
||||
```
|
||||
AzureADMultipleOrgs Security Investigator
|
||||
AzureADMultipleOrgs Exchange Operator
|
||||
AzureADMultipleOrgs User Manager
|
||||
AzureADMultipleOrgs Tenant Admin
|
||||
AzureADMultipleOrgs Defender Add-on
|
||||
AzureADMyOrg Intune Manager <-- the odd one
|
||||
```
|
||||
|
||||
**Correcting my earlier ack above:** I chalked the grabblaw `AADSTS700016` up to "app not consented in that tenant — same category as defender." That diagnosis was wrong. `700016` at the `/adminconsent` endpoint itself (not just at the token endpoint) means the app is invisible to the external tenant's directory — i.e., the audience blocks it before any consent UI even loads. Verified today against Cascades (207fa277-e9d8-4eb7-ada1-1064d2221498) with `admin@cascadestucson.com` — same 700016 straight from the sign-in screen.
|
||||
|
||||
**Current impact:** I'm blocked on Cascades MDM phone setup. Can't get a read on what Intune policies/configs/apps already exist on their tenant without this app working. Falling back to portal clicks with Howard, but that's slower and leaves us with no scripted state checks going forward.
|
||||
|
||||
**Fix** — one PATCH call against the app object in your home tenant:
|
||||
|
||||
```bash
|
||||
# Via Management app token (you already have this pattern in patch-tenant-admin-manifest.sh)
|
||||
curl -X PATCH -H "Authorization: Bearer $MGMT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://graph.microsoft.com/v1.0/applications/31017446-c01a-4775-864f-aef96ce43797" \
|
||||
-d '{"signInAudience": "AzureADMultipleOrgs"}'
|
||||
```
|
||||
|
||||
Or in the portal: Entra → App registrations → ComputerGuru - Intune Manager → Authentication → **Supported account types** → pick "Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant)" → Save.
|
||||
|
||||
**Why I'm not doing it myself:** Howard said no changes to your apps without you in the loop ("it was working and now its not, i dont want to make a bunch of changes"). Ball's in your court — takes ~30 seconds.
|
||||
|
||||
**After you flip it, I'll:**
|
||||
1. Re-click the consent URL with Cascades GA, create the SP + grant scopes
|
||||
2. Run the Intune readout against Cascades
|
||||
3. Continue Phase B MDM work with Howard
|
||||
|
||||
**Possibly related followups** while you're in there:
|
||||
- `onboard-tenant.sh` still only auto-consents the original 5 apps. Needs `intune-manager` added so future tenants onboard cleanly.
|
||||
- `references/tenants.md` consent URL section doesn't have an Intune Manager template yet.
|
||||
- `SKILL.md` tier table lists 6 tiers, actual is 7.
|
||||
|
||||
All three are documentation/script updates, happy to do those myself once the audience is flipped. Let me know.
|
||||
@@ -1,118 +1,284 @@
|
||||
#!/bin/bash
|
||||
# ClaudeTools Bidirectional Sync Script
|
||||
# Ensures proper pull BEFORE push on all machines
|
||||
# Prints incoming/outgoing change summary with author attribution
|
||||
|
||||
set -e # Exit on error
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Detect machine name
|
||||
# Machine + timestamp
|
||||
if [ -n "$COMPUTERNAME" ]; then
|
||||
MACHINE="$COMPUTERNAME"
|
||||
else
|
||||
MACHINE=$(hostname)
|
||||
fi
|
||||
|
||||
# Timestamp
|
||||
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
|
||||
|
||||
echo -e "${GREEN}[OK]${NC} Starting ClaudeTools sync from $MACHINE at $TIMESTAMP"
|
||||
|
||||
# Navigate to ClaudeTools directory
|
||||
if [ -d "$HOME/ClaudeTools" ]; then
|
||||
cd "$HOME/ClaudeTools"
|
||||
elif [ -d "/d/ClaudeTools" ]; then
|
||||
cd "/d/ClaudeTools"
|
||||
elif [ -d "D:/ClaudeTools" ]; then
|
||||
cd "D:/ClaudeTools"
|
||||
else
|
||||
echo -e "${RED}[ERROR]${NC} ClaudeTools directory not found"
|
||||
# Navigate to ClaudeTools directory (check common locations)
|
||||
for candidate in "$HOME/ClaudeTools" "/d/ClaudeTools" "D:/ClaudeTools" "/d/claudetools" "D:/claudetools"; do
|
||||
if [ -d "$candidate" ]; then
|
||||
cd "$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ! -d ".git" ]; then
|
||||
echo -e "${RED}[ERROR]${NC} Not in a git working tree"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}[OK]${NC} Working directory: $(pwd)"
|
||||
|
||||
# Phase 1: Check and commit local changes
|
||||
echo ""
|
||||
echo "=== Phase 1: Local Changes ==="
|
||||
|
||||
if ! git diff-index --quiet HEAD -- 2>/dev/null; then
|
||||
echo -e "${YELLOW}[INFO]${NC} Local changes detected"
|
||||
|
||||
# Show status
|
||||
git status --short
|
||||
|
||||
# Stage all changes
|
||||
echo -e "${GREEN}[OK]${NC} Staging all changes..."
|
||||
git add -A
|
||||
|
||||
# Commit with timestamp
|
||||
COMMIT_MSG="sync: Auto-sync from $MACHINE at $TIMESTAMP
|
||||
|
||||
Synced files:
|
||||
- Session logs updated
|
||||
- Latest context and credentials
|
||||
- Command/directive updates
|
||||
|
||||
Machine: $MACHINE
|
||||
Timestamp: $TIMESTAMP
|
||||
|
||||
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>"
|
||||
|
||||
git commit -m "$COMMIT_MSG"
|
||||
echo -e "${GREEN}[OK]${NC} Changes committed"
|
||||
else
|
||||
echo -e "${GREEN}[OK]${NC} No local changes to commit"
|
||||
fi
|
||||
|
||||
# Phase 2: Sync with remote (CRITICAL: Pull BEFORE Push)
|
||||
echo ""
|
||||
echo "=== Phase 2: Remote Sync (Pull + Push) ==="
|
||||
|
||||
# Fetch to see what's available
|
||||
echo -e "${GREEN}[OK]${NC} Fetching from remote..."
|
||||
git fetch origin
|
||||
|
||||
# Check if remote has updates
|
||||
LOCAL=$(git rev-parse main)
|
||||
REMOTE=$(git rev-parse origin/main)
|
||||
|
||||
if [ "$LOCAL" != "$REMOTE" ]; then
|
||||
echo -e "${YELLOW}[INFO]${NC} Remote has updates, pulling..."
|
||||
|
||||
# Pull with rebase
|
||||
if git pull origin main --rebase; then
|
||||
echo -e "${GREEN}[OK]${NC} Successfully pulled remote changes"
|
||||
git log --oneline "$LOCAL..origin/main"
|
||||
else
|
||||
echo -e "${RED}[ERROR]${NC} Pull failed - may have conflicts"
|
||||
echo -e "${YELLOW}[INFO]${NC} Resolve conflicts and run sync again"
|
||||
exit 1
|
||||
# Detect Python interpreter — verify it actually runs (Windows Store stub passes command -v but fails to execute)
|
||||
PYTHON=""
|
||||
for candidate in py python3 python; do
|
||||
if command -v "$candidate" >/dev/null 2>&1; then
|
||||
if "$candidate" -c "import sys; sys.exit(0)" >/dev/null 2>&1; then
|
||||
PYTHON="$candidate"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}[OK]${NC} Already up to date with remote"
|
||||
fi
|
||||
|
||||
# Push local changes
|
||||
echo ""
|
||||
echo -e "${GREEN}[OK]${NC} Pushing local changes to remote..."
|
||||
if git push origin main; then
|
||||
echo -e "${GREEN}[OK]${NC} Successfully pushed to remote"
|
||||
else
|
||||
echo -e "${RED}[ERROR]${NC} Push failed"
|
||||
done
|
||||
if [ -z "$PYTHON" ]; then
|
||||
echo -e "${RED}[ERROR]${NC} No Python interpreter found (tried: py, python3, python)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Phase 3: Report final status
|
||||
# Load user identity
|
||||
USER_DISPLAY="unknown"
|
||||
USER_GITEA=""
|
||||
if [ -f ".claude/identity.json" ]; then
|
||||
USER_DISPLAY=$($PYTHON -c "import json,sys; d=json.load(open('.claude/identity.json')); print(d.get('full_name', d.get('user','unknown')))" 2>/dev/null || echo "unknown")
|
||||
USER_GITEA=$($PYTHON -c "import json,sys; d=json.load(open('.claude/identity.json')); print(d.get('user',''))" 2>/dev/null || echo "")
|
||||
fi
|
||||
echo -e "${GREEN}[OK]${NC} Syncing as: $USER_DISPLAY (machine: $MACHINE)"
|
||||
|
||||
# Phase 1: Local changes
|
||||
echo ""
|
||||
echo "=== Sync Complete ==="
|
||||
echo -e "${GREEN}[OK]${NC} Local branch: $(git rev-parse --abbrev-ref HEAD)"
|
||||
echo -e "${GREEN}[OK]${NC} Current commit: $(git log -1 --oneline)"
|
||||
echo -e "${GREEN}[OK]${NC} Remote status: $(git status -sb | head -1)"
|
||||
echo "=== Phase 1: Local changes ==="
|
||||
|
||||
if ! git diff-index --quiet HEAD -- 2>/dev/null; then
|
||||
echo -e "${YELLOW}[INFO]${NC} Local changes detected:"
|
||||
git status --short
|
||||
echo ""
|
||||
|
||||
echo -e "${GREEN}[OK]${NC} Staging all changes..."
|
||||
git add -A
|
||||
|
||||
# Commit message (Co-Authored-By uses local git user if configured)
|
||||
COMMIT_MSG="sync: auto-sync from $MACHINE at $TIMESTAMP
|
||||
|
||||
Author: $USER_DISPLAY
|
||||
Machine: $MACHINE
|
||||
Timestamp: $TIMESTAMP"
|
||||
|
||||
if git diff-index --quiet --cached HEAD -- 2>/dev/null; then
|
||||
echo -e "${GREEN}[OK]${NC} No stageable changes (submodule internal changes skipped)."
|
||||
else
|
||||
git commit -m "$COMMIT_MSG"
|
||||
echo -e "${GREEN}[OK]${NC} Committed."
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}[OK]${NC} No local changes to commit."
|
||||
fi
|
||||
|
||||
# Phase 2: Remote sync
|
||||
echo ""
|
||||
echo "=== Phase 2: Fetch + inspect ==="
|
||||
|
||||
LOCAL_BEFORE=$(git rev-parse HEAD)
|
||||
|
||||
echo -e "${GREEN}[OK]${NC} Fetching from origin..."
|
||||
git fetch origin --quiet
|
||||
|
||||
LOCAL=$(git rev-parse HEAD)
|
||||
REMOTE=$(git rev-parse origin/main 2>/dev/null || git rev-parse origin/master 2>/dev/null || echo "$LOCAL")
|
||||
REMOTE_BRANCH="origin/main"
|
||||
if ! git rev-parse origin/main >/dev/null 2>&1; then
|
||||
REMOTE_BRANCH="origin/master"
|
||||
fi
|
||||
|
||||
# Count and show incoming
|
||||
INCOMING_COUNT=$(git rev-list --count HEAD..$REMOTE_BRANCH 2>/dev/null || echo 0)
|
||||
OUTGOING_COUNT=$(git rev-list --count $REMOTE_BRANCH..HEAD 2>/dev/null || echo 0)
|
||||
|
||||
if [ "$INCOMING_COUNT" -gt 0 ]; then
|
||||
echo ""
|
||||
echo -e "${CYAN}--- Incoming: $INCOMING_COUNT commits from remote ---${NC}"
|
||||
git log --oneline --format=' %C(yellow)%h%Creset %C(cyan)%an%Creset %s %C(dim)(%ar)%Creset' HEAD..$REMOTE_BRANCH | head -30
|
||||
echo ""
|
||||
echo -e "${CYAN}--- Files touched by incoming commits ---${NC}"
|
||||
git diff --stat HEAD..$REMOTE_BRANCH | tail -20
|
||||
else
|
||||
echo -e "${GREEN}[OK]${NC} No incoming changes."
|
||||
fi
|
||||
|
||||
if [ "$OUTGOING_COUNT" -gt 0 ]; then
|
||||
echo ""
|
||||
echo -e "${CYAN}--- Outgoing: $OUTGOING_COUNT commits to remote ---${NC}"
|
||||
git log --oneline --format=' %C(yellow)%h%Creset %C(cyan)%an%Creset %s %C(dim)(%ar)%Creset' $REMOTE_BRANCH..HEAD | head -30
|
||||
fi
|
||||
|
||||
# Phase 3: Pull (if needed)
|
||||
if [ "$INCOMING_COUNT" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "=== Phase 3: Pull (rebase) ==="
|
||||
if git pull origin main --rebase; then
|
||||
echo -e "${GREEN}[OK]${NC} Pulled successfully."
|
||||
else
|
||||
echo -e "${RED}[ERROR]${NC} Pull failed (likely conflicts). Resolve and re-run sync."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Phase 4: Push (if needed)
|
||||
OUTGOING_AFTER_PULL=$(git rev-list --count $REMOTE_BRANCH..HEAD 2>/dev/null || echo 0)
|
||||
if [ "$OUTGOING_AFTER_PULL" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "=== Phase 4: Push ==="
|
||||
if git push origin main; then
|
||||
echo -e "${GREEN}[OK]${NC} Pushed successfully."
|
||||
else
|
||||
echo -e "${RED}[ERROR]${NC} Push failed. Check auth / network."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}[OK]${NC} Nothing to push."
|
||||
fi
|
||||
|
||||
# Phase 5: Scan pulled session logs for cross-user messages
|
||||
# Look for "## Note for" or "## Message for" sections in any session log
|
||||
# touched by incoming commits. Print them prominently so they aren't missed.
|
||||
if [ "$INCOMING_COUNT" -gt 0 ] && [ -n "$LOCAL_BEFORE" ]; then
|
||||
CHANGED_LOGS=$(git diff --name-only "$LOCAL_BEFORE"..HEAD -- '**/session-logs/*.md' 'session-logs/*.md' 2>/dev/null || true)
|
||||
if [ -n "$CHANGED_LOGS" ]; then
|
||||
NOTES_FOUND=0
|
||||
for LOG_FILE in $CHANGED_LOGS; do
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
# Extract author from "## User" block and any "## Note for" / "## Message for" sections
|
||||
NOTE_CONTENT=$(awk '
|
||||
/^## (Note|Message) for /{ in_note=1; header=$0; next }
|
||||
in_note && /^## /{ in_note=0 }
|
||||
in_note{ buf=buf"\n"$0 }
|
||||
END{ if(buf) print header buf }
|
||||
' "$LOG_FILE")
|
||||
if [ -n "$NOTE_CONTENT" ]; then
|
||||
if [ "$NOTES_FOUND" -eq 0 ]; then
|
||||
echo ""
|
||||
echo -e "${YELLOW}============================================================${NC}"
|
||||
echo -e "${YELLOW} MESSAGES FROM OTHER TEAM MEMBERS${NC}"
|
||||
echo -e "${YELLOW}============================================================${NC}"
|
||||
NOTES_FOUND=1
|
||||
fi
|
||||
LOG_AUTHOR=$(awk '/^- \*\*User:\*\*/{print; exit}' "$LOG_FILE" | sed 's/.*\*\*User:\*\* //')
|
||||
LOG_DATE=$(basename "$LOG_FILE" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' | head -1)
|
||||
echo ""
|
||||
echo -e "${YELLOW} From: ${LOG_AUTHOR:-unknown} | ${LOG_DATE:-unknown date} | ${LOG_FILE}${NC}"
|
||||
echo -e "${YELLOW}------------------------------------------------------------${NC}"
|
||||
echo "$NOTE_CONTENT"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ "$NOTES_FOUND" -gt 0 ]; then
|
||||
echo ""
|
||||
echo -e "${YELLOW}============================================================${NC}"
|
||||
echo -e "${YELLOW} Address the above before continuing with other work.${NC}"
|
||||
echo -e "${YELLOW}============================================================${NC}"
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Phase 6: Vault sync
|
||||
echo ""
|
||||
echo "=== Phase 6: Vault sync ==="
|
||||
|
||||
VAULT_PATH=""
|
||||
if [ -f ".claude/identity.json" ]; then
|
||||
VAULT_PATH=$($PYTHON -c "import json; d=json.load(open('.claude/identity.json')); print(d.get('vault_path',''))" 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
if [ -z "$VAULT_PATH" ]; then
|
||||
echo -e "${YELLOW}[INFO]${NC} vault_path not set in identity.json — skipping vault sync."
|
||||
elif [ ! -d "$VAULT_PATH/.git" ]; then
|
||||
echo -e "${YELLOW}[WARNING]${NC} Vault path '$VAULT_PATH' is not a git repo — skipping."
|
||||
else
|
||||
CLAUDETOOLS_DIR=$(pwd)
|
||||
cd "$VAULT_PATH"
|
||||
echo -e "${GREEN}[OK]${NC} Vault: $VAULT_PATH"
|
||||
|
||||
# Commit any local vault changes
|
||||
if ! git diff-index --quiet HEAD -- 2>/dev/null; then
|
||||
echo -e "${YELLOW}[INFO]${NC} Local vault changes detected — committing..."
|
||||
git add -A
|
||||
git commit -m "sync: auto-sync vault from $MACHINE at $TIMESTAMP"
|
||||
echo -e "${GREEN}[OK]${NC} Vault committed."
|
||||
else
|
||||
echo -e "${GREEN}[OK]${NC} No local vault changes."
|
||||
fi
|
||||
|
||||
VAULT_LOCAL_BEFORE=$(git rev-parse HEAD)
|
||||
git fetch origin --quiet
|
||||
|
||||
VAULT_REMOTE_BRANCH="origin/main"
|
||||
if ! git rev-parse origin/main >/dev/null 2>&1; then
|
||||
VAULT_REMOTE_BRANCH="origin/master"
|
||||
fi
|
||||
|
||||
VAULT_INCOMING=$(git rev-list --count HEAD..$VAULT_REMOTE_BRANCH 2>/dev/null || echo 0)
|
||||
VAULT_OUTGOING=$(git rev-list --count $VAULT_REMOTE_BRANCH..HEAD 2>/dev/null || echo 0)
|
||||
|
||||
if [ "$VAULT_INCOMING" -gt 0 ]; then
|
||||
echo -e "${CYAN}--- Vault: $VAULT_INCOMING incoming commit(s) ---${NC}"
|
||||
git log --oneline --format=' %C(yellow)%h%Creset %C(cyan)%an%Creset %s %C(dim)(%ar)%Creset' HEAD..$VAULT_REMOTE_BRANCH | head -10
|
||||
if git pull origin main --rebase; then
|
||||
echo -e "${GREEN}[OK]${NC} Vault pulled."
|
||||
else
|
||||
echo -e "${RED}[ERROR]${NC} Vault pull failed — resolve conflicts manually in $VAULT_PATH."
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}[OK]${NC} Vault: no incoming changes."
|
||||
fi
|
||||
|
||||
VAULT_OUTGOING_AFTER=$(git rev-list --count $VAULT_REMOTE_BRANCH..HEAD 2>/dev/null || echo 0)
|
||||
if [ "$VAULT_OUTGOING_AFTER" -gt 0 ]; then
|
||||
if git push origin main; then
|
||||
echo -e "${GREEN}[OK]${NC} Vault pushed ($VAULT_OUTGOING_AFTER commit(s))."
|
||||
else
|
||||
echo -e "${RED}[ERROR]${NC} Vault push failed."
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}[OK]${NC} Vault: nothing to push."
|
||||
fi
|
||||
|
||||
cd "$CLAUDETOOLS_DIR"
|
||||
fi
|
||||
|
||||
# Phase 7: Summary
|
||||
echo ""
|
||||
echo "=== Sync Summary ==="
|
||||
|
||||
if [ "$INCOMING_COUNT" -gt 0 ]; then
|
||||
INCOMING_AUTHORS=$(git log --format='%an' $LOCAL_BEFORE..HEAD 2>/dev/null | sort | uniq -c | sort -rn | awk '{printf "%s (%s), ", substr($0, index($0,$2)), $1}' | sed 's/, $//')
|
||||
echo -e "${CYAN}Pulled in:${NC} $INCOMING_COUNT commit(s) — authors: ${INCOMING_AUTHORS:-unknown}"
|
||||
fi
|
||||
if [ "$OUTGOING_AFTER_PULL" -gt 0 ]; then
|
||||
echo -e "${CYAN}Pushed out:${NC} $OUTGOING_AFTER_PULL commit(s) by $USER_DISPLAY"
|
||||
fi
|
||||
if [ "$INCOMING_COUNT" -eq 0 ] && [ "$OUTGOING_AFTER_PULL" -eq 0 ]; then
|
||||
echo -e "${GREEN}Already in sync — no commits moved in either direction.${NC}"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}[OK]${NC} HEAD: $(git log -1 --oneline)"
|
||||
echo -e "${GREEN}[OK]${NC} Status: $(git status -sb | head -1)"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}[SUCCESS]${NC} All machines in sync. Ready to continue work."
|
||||
echo -e "${GREEN}[SUCCESS]${NC} Sync complete."
|
||||
|
||||
54
.claude/scripts/vault.sh
Normal file
54
.claude/scripts/vault.sh
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# vault.sh — ClaudeTools wrapper for the SOPS vault.
|
||||
#
|
||||
# Reads vault_path from .claude/identity.json (per-machine, gitignored).
|
||||
# Delegates all arguments to the real vault.sh in that directory.
|
||||
#
|
||||
# Usage (from any directory):
|
||||
# bash "$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel)/.claude/scripts/vault.sh" get-field <path> <field>
|
||||
#
|
||||
# Or set CLAUDETOOLS_ROOT and call directly:
|
||||
# bash "$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh" get-field <path> <field>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CLAUDETOOLS_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
IDENTITY_FILE="$CLAUDETOOLS_ROOT/.claude/identity.json"
|
||||
|
||||
if [[ ! -f "$IDENTITY_FILE" ]]; then
|
||||
echo "[ERROR] .claude/identity.json not found at $IDENTITY_FILE" >&2
|
||||
echo " Run onboarding to create it, or add vault_path manually." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract vault_path from identity.json — jq first, then Python with path conversion
|
||||
VAULT_ROOT=""
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
VAULT_ROOT=$(jq -r '.vault_path // empty' "$IDENTITY_FILE" 2>/dev/null)
|
||||
fi
|
||||
if [[ -z "$VAULT_ROOT" ]]; then
|
||||
IDENTITY_FILE_FOR_PY="$IDENTITY_FILE"
|
||||
command -v cygpath >/dev/null 2>&1 && IDENTITY_FILE_FOR_PY=$(cygpath -m "$IDENTITY_FILE")
|
||||
for py in py python3 python; do
|
||||
if command -v "$py" >/dev/null 2>&1; then
|
||||
VAULT_ROOT=$("$py" -c "import json,sys; d=json.load(open(r'$IDENTITY_FILE_FOR_PY')); print(d.get('vault_path',''))" 2>/dev/null) && break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -z "$VAULT_ROOT" ]]; then
|
||||
echo "[ERROR] vault_path not set in $IDENTITY_FILE" >&2
|
||||
echo " Add: \"vault_path\": \"/path/to/vault\"" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REAL_VAULT_SH="$VAULT_ROOT/scripts/vault.sh"
|
||||
|
||||
if [[ ! -f "$REAL_VAULT_SH" ]]; then
|
||||
echo "[ERROR] vault.sh not found at $REAL_VAULT_SH" >&2
|
||||
echo " Check vault_path in $IDENTITY_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec bash "$REAL_VAULT_SH" "$@"
|
||||
9
.claude/settings.json
Normal file
9
.claude/settings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"defaultMode": "bypassPermissions"
|
||||
},
|
||||
"preferences": {
|
||||
"autoCompact": true,
|
||||
"verbose": false
|
||||
}
|
||||
}
|
||||
@@ -154,18 +154,12 @@ op vault list --format=json # Vaults as JSON
|
||||
## Useful Patterns
|
||||
|
||||
```bash
|
||||
# Find item by field value (search)
|
||||
op item list --format=json | \
|
||||
python3 -c "import sys,json; [print(i['title']) for i in json.load(sys.stdin)]"
|
||||
|
||||
# Export all items in a vault to JSON (backup)
|
||||
op item list --vault Dev --format=json | \
|
||||
python3 -c "import sys,json; ids=[i['id'] for i in json.load(sys.stdin)]"
|
||||
# (then loop to get each)
|
||||
# List item titles
|
||||
op item list --format=json | jq -r '.[].title'
|
||||
|
||||
# Check if a specific item exists
|
||||
op item get "My Item" &>/dev/null && echo "exists" || echo "not found"
|
||||
|
||||
# Get item ID (for scripting)
|
||||
op item get "My Item" --format=json | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])"
|
||||
op item get "My Item" --format=json | jq -r '.id'
|
||||
```
|
||||
|
||||
@@ -102,8 +102,7 @@ op run --env-file=n8n.env.tpl -- docker compose up n8n
|
||||
|
||||
```bash
|
||||
# List all fields in an item
|
||||
op item get "Item Name" --format=json | \
|
||||
python3 -c "import sys,json; [print(f['label']) for f in json.load(sys.stdin)['fields'] if f.get('value')]"
|
||||
op item get "Item Name" --format=json | jq -r '.fields[] | select(.value) | .label'
|
||||
|
||||
# Or view interactively
|
||||
op item get "Item Name"
|
||||
|
||||
@@ -50,7 +50,7 @@ if op account list &>/dev/null 2>&1; then
|
||||
echo ""
|
||||
echo " Vaults:"
|
||||
op vault list --format=json 2>/dev/null | \
|
||||
python3 -c "import sys,json; [print(f' • {v[\"name\"]} ({v[\"id\"]})') for v in json.load(sys.stdin)]" 2>/dev/null || true
|
||||
jq -r '.[] | " \u2022 \(.name) (\(.id))"' 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
@@ -42,11 +42,9 @@ fi
|
||||
if [[ -z "$ITEM" ]]; then
|
||||
echo "Available items in vault '${VAULT:-all vaults}':"
|
||||
if [[ -n "$VAULT" ]]; then
|
||||
op item list --vault "$VAULT" --format=json | \
|
||||
python3 -c "import sys,json; [print(f' {i[\"title\"]}') for i in json.load(sys.stdin)]"
|
||||
op item list --vault "$VAULT" --format=json | jq -r '.[] | " \(.title)"'
|
||||
else
|
||||
op item list --format=json | \
|
||||
python3 -c "import sys,json; [print(f' [{i[\"vault\"][\"name\"]}] {i[\"title\"]}') for i in json.load(sys.stdin)]"
|
||||
op item list --format=json | jq -r '.[] | " [\(.vault.name)] \(.title)"'
|
||||
fi
|
||||
echo ""
|
||||
read -rp "Enter item title: " ITEM
|
||||
@@ -61,11 +59,11 @@ else
|
||||
ITEM_JSON=$(op item get "$ITEM" --format=json)
|
||||
fi
|
||||
|
||||
VAULT_NAME=$(echo "$ITEM_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['vault']['name'])")
|
||||
ITEM_TITLE=$(echo "$ITEM_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['title'])")
|
||||
VAULT_NAME=$(echo "$ITEM_JSON" | jq -r '.vault.name')
|
||||
ITEM_TITLE=$(echo "$ITEM_JSON" | jq -r '.title')
|
||||
|
||||
# Build .env content
|
||||
ENV_CONTENT=$(echo "$ITEM_JSON" | python3 - <<'PYEOF'
|
||||
ENV_CONTENT=$(echo "$ITEM_JSON" | py - <<'PYEOF'
|
||||
import sys, json, re
|
||||
|
||||
data = json.load(sys.stdin)
|
||||
|
||||
@@ -78,8 +78,8 @@ else
|
||||
"${FIELD}[password]=${VALUE}" \
|
||||
--format=json)
|
||||
|
||||
ITEM_ID=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
||||
VAULT_NAME=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin)['vault']['name'])")
|
||||
ITEM_ID=$(echo "$RESULT" | jq -r '.id')
|
||||
VAULT_NAME=$(echo "$RESULT" | jq -r '.vault.name')
|
||||
|
||||
echo "✅ Created '${TITLE}' (ID: ${ITEM_ID})"
|
||||
echo ""
|
||||
|
||||
64
.claude/skills/remediation-tool/SKILL.md
Normal file
64
.claude/skills/remediation-tool/SKILL.md
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: remediation-tool
|
||||
description: |
|
||||
M365 tenant investigation and remediation using the ComputerGuru tiered MSP app suite (5 apps: Security Investigator, Exchange Operator, User Manager, Tenant Admin, Defender Add-on). Auto-invoke when the user says "remediation tool", "365 remediation", "check <user>'s mailbox/box", "credential stuffing" against an M365 user, "breach check" on an M365 tenant, or needs M365 admin API work that client-credentials Graph + Exchange REST can perform. NOT for CIPP — this is the direct Graph API app suite.
|
||||
|
||||
Also invoke when the user needs any of: inbox rule enumeration, mailbox forwarding check, delegate/SendAs audit, OAuth consent audit, sign-in log queries, risky user lookup, directory audit queries, B2B guest invite audit against M365.
|
||||
|
||||
Triggers: "365 remediation", "remediation tool", "check <user> box/mailbox/account for breach", "credential stuff*", "who's getting attacked", "foreign sign-in", "inbox rule", "mailbox forward*", "oauth consent" (in MSP context), "tenant sweep", "risky user", "hidden rule", Exchange Online admin API, "adminapi/beta/{tenant}/InvokeCommand".
|
||||
---
|
||||
|
||||
# 365 Remediation Tool
|
||||
|
||||
Read-only by default. All remediation actions require explicit `YES` confirmation in chat (not a permission prompt).
|
||||
|
||||
## App Architecture (Tiered)
|
||||
|
||||
Five multi-tenant apps cover distinct privilege tiers. Use only what the task requires.
|
||||
|
||||
| Tier | App display name | App ID | Vault file | Scope |
|
||||
|---|---|---|---|---|
|
||||
| `investigator` | ComputerGuru Security Investigator | `bfbc12a4-f0dd-4e12-b06d-997e7271e10c` | `computerguru-security-investigator.sops.yaml` | Graph read-only |
|
||||
| `investigator-exo` | ComputerGuru Security Investigator | `bfbc12a4-f0dd-4e12-b06d-997e7271e10c` | `computerguru-security-investigator.sops.yaml` | Exchange Online read |
|
||||
| `exchange-op` | ComputerGuru Exchange Operator | `b43e7342-5b4b-492f-890f-bb5a4f7f40e9` | `computerguru-exchange-operator.sops.yaml` | Exchange Online write |
|
||||
| `user-manager` | ComputerGuru User Manager | `64fac46b-8b44-41ad-93ee-7da03927576c` | `computerguru-user-manager.sops.yaml` | Graph user/group write |
|
||||
| `tenant-admin` | ComputerGuru Tenant Admin | `709e6eed-0711-4875-9c44-2d3518c47063` | `computerguru-tenant-admin.sops.yaml` | Graph high-privilege |
|
||||
| `defender` | ComputerGuru Defender Add-on | `dbf8ad1a-54f4-4bb8-8a9e-ea5b9634635b` | `computerguru-defender-addon.sops.yaml` | Defender ATP (MDE only) |
|
||||
|
||||
**Default for breach checks:** use `investigator` (Graph) + `investigator-exo` (Exchange read). Escalate to write tiers only when remediating.
|
||||
|
||||
## Auto-Invocation Behavior
|
||||
|
||||
When triggered automatically (vs. via `/remediation-tool`), follow the same workflow in `.claude/commands/remediation-tool.md`:
|
||||
|
||||
1. Parse the user's intent into a subcommand (check/sweep/signins/consent-url/remediate).
|
||||
2. Resolve tenant ID from domain.
|
||||
3. Acquire tokens via `get-token.sh <tenant> <tier>` — use lowest-privilege tier needed.
|
||||
4. Run checks via scripts in `scripts/`.
|
||||
5. Interpret findings using `references/checklist.md`.
|
||||
6. Write report to `clients/{slug}/reports/YYYY-MM-DD-{action}.md` using `templates/breach-report.md`.
|
||||
7. Chat summary + delegate commit to Gitea agent.
|
||||
|
||||
## Before calling any script, verify
|
||||
|
||||
- The SOPS vault is accessible: `test -f D:/vault/scripts/vault.sh` (Windows) or `test -f ~/vault/scripts/vault.sh` (other).
|
||||
- `jq`, `curl`, `bash` are available.
|
||||
- For Exchange REST checks: confirm the target tenant has **Exchange Administrator** role assigned to the **Security Investigator** SP (for reads) or **Exchange Operator** SP (for writes). If any Exchange REST call returns 403, emit the tenant-scoped Entra Roles link from `references/gotchas.md`.
|
||||
- For Identity Protection checks: `IdentityRiskyUser.Read.All` is in the Security Investigator manifest AND the tenant has consented to that app. If 403, emit the per-app consent URL from `references/gotchas.md`.
|
||||
- For Defender checks: confirm tenant has Microsoft Defender for Endpoint (MDE) license before using `defender` tier — it returns AADSTS650052 otherwise.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Target identifiers**: accept UPN, domain, or tenant GUID. Normalize to tenant GUID internally.
|
||||
- **Token tiers**: minimum necessary privilege. Never use `tenant-admin` for a read-only check.
|
||||
- **Token cache**: `/tmp/remediation-tool/{tenant-id}/{tier}.jwt`. TTL 55 minutes. Check `-mmin -55` before reuse.
|
||||
- **Raw JSON artifacts**: `/tmp/remediation-tool/{tenant-id}/{check}/` — keep so the user can re-analyze.
|
||||
- **Reports**: `clients/{slug}/reports/YYYY-MM-DD-{action}.md`. Derive slug from domain (strip TLD, hyphenate).
|
||||
- **UTC dates everywhere**.
|
||||
|
||||
## Scope boundaries
|
||||
|
||||
- **Not a replacement for CIPP.** Use CIPP for bulk baseline configuration, templates, standards alerting. Use this tool for focused investigation and point-in-time remediation.
|
||||
- **Not for creating/modifying Entra apps or Conditional Access policies.** Those are sensitive enough to stay manual in the portal.
|
||||
- **Not for Graph permissions the apps don't have.** If a call 403s and the scope isn't in the relevant app's manifest, stop and tell the user — don't try to work around it.
|
||||
- **Defender tier requires MDE license.** If the tenant doesn't have MDE, the token request succeeds but API calls return AADSTS650052. Check before using.
|
||||
48
.claude/skills/remediation-tool/references/checklist.md
Normal file
48
.claude/skills/remediation-tool/references/checklist.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Breach-Check Rubric
|
||||
|
||||
How to interpret the outputs from `user-breach-check.sh` and `tenant-sweep.sh`.
|
||||
|
||||
## Single-user check — the 10 points
|
||||
|
||||
| # | Check | What "clean" looks like | Red flags |
|
||||
|---|---|---|---|
|
||||
| 1 | Inbox rules (Graph) | Empty, or only benign filters | ForwardTo / RedirectTo / ForwardAsAttachmentTo set; DeleteMessage+MarkAsRead combos; rules filtered on "password", "bank", "invoice", "CEO name", "security"; rules with name like "." or " " (attacker hiding) |
|
||||
| 2 | Mailbox settings / auto-reply | Auto-reply disabled or legitimate | Auto-reply active with external audience + unfamiliar message body |
|
||||
| 3 | Exchange REST (hidden rules, delegates, SendAs, Get-Mailbox forwarding fields) | Only SELF in permissions; no forwarding | **Hidden** inbox rule moving to RSS/Notes/Conversation History; non-SELF FullAccess/SendAs; ForwardingAddress or ForwardingSmtpAddress set to external |
|
||||
| 4 | OAuth consents + app role assignments | Legitimate apps only (Teams, Outlook mobile, BlueMail, etc.); dates match user history | New consent in attack window; unknown app with `Mail.ReadWrite`, `Files.ReadWrite`, `offline_access`; publisher not verified |
|
||||
| 5 | Auth methods | All methods predate the attack window | New phone/Authenticator registered within hours of first suspicious sign-in; duplicate entries with the same device name but different createdDateTime |
|
||||
| 6 | Sign-ins 30d | Consistent US IPs, user's known geography | Any successful sign-in from a country the user never visits; IMAP/POP/Authenticated SMTP client apps (legacy auth); sign-ins from TOR exit nodes or known residential-proxy ranges |
|
||||
| 7 | Directory audits | Only legit admin/system actions | `Update user` by non-admin principal; password reset the user didn't initiate; auth method change from `Microsoft Substrate Management` is normal but repeated changes are not |
|
||||
| 8 | Risky users / risk detections | `riskLevel: none` | Any `medium` or `high`; `riskDetail: userPerformedSecuredPasswordChange` just means resolved — check the original detection |
|
||||
| 9 | Sent items (recent 25) | Normal business correspondence | Blast emails to random external recipients; forwards of internal financial/HR info externally; anything after-hours from an unusual client app |
|
||||
| 10 | Deleted items (recent 25) | Marketing/spam, routine notifications | Deleted security alerts, password-reset emails, MFA notifications, bounce notices the user wouldn't delete — all signs of attacker cleanup |
|
||||
|
||||
### Cross-check rule
|
||||
|
||||
If inbox rules and forwarding are clean **but** sign-ins show successful foreign access — attacker may have used OAuth-based access (check OAuth grants) or already extracted data and cleaned up. Pull sent items + deleted items aggressively and check `/auditLogs/signIns/beta` for non-interactive sign-ins.
|
||||
|
||||
## Tenant-wide sweep — priorities
|
||||
|
||||
| Priority | Signal | Action |
|
||||
|---|---|---|
|
||||
| P1 | User with ≥20 failed sign-ins from ≥2 foreign countries | Likely active credential-stuffing target. Reset password, disable SMTP AUTH, monitor. |
|
||||
| P1 | Successful sign-in from non-US | Verify with user immediately. If not them: force password reset + revoke sessions + full breach check. |
|
||||
| P2 | New OAuth consent to unfamiliar app in attack window | Review app publisher, scopes, and requesting user. Revoke if unknown. |
|
||||
| P2 | B2B guest invite to personal email domain (gmail.com, outlook.com, yahoo.com) | Confirm with inviter it's intentional. Guest invites are a known persistence mechanism. |
|
||||
| P3 | Transport rule created/modified by a non-admin | Transport rules can redirect mail tenant-wide. Review body/actions carefully. |
|
||||
| P3 | Service principal added by non-admin or by "PowerApps Service" unexpectedly | Usually benign, but worth noting. |
|
||||
| P4 | Isolated wrong-password attempt from foreign IP | Record and move on. Single attempts are noise unless repeated. |
|
||||
|
||||
## False positives to filter out
|
||||
|
||||
- `sysadmin@<tenant>` failures during onboarding (error 65001 against any **ComputerGuru** app — Security Investigator, Exchange Operator, User Manager, Tenant Admin, or Defender Add-on).
|
||||
- `Microsoft Substrate Management` and `Azure MFA StrongAuthenticationService` routinely update user records — those are not attacker activity.
|
||||
- Our own consent attempts show up as `Consent to application` in directory audits. Filter `sysadmin` + target matching any "ComputerGuru" app display name during the onboarding window.
|
||||
- `error 50140` "Keep me signed in interrupt" is a browser prompt, not a failed auth.
|
||||
|
||||
## When to escalate beyond this tool
|
||||
|
||||
- Data exfiltration suspected -> pull Unified Audit Log via Purview (this tool does not access UAL).
|
||||
- Tenant-wide phishing campaign -> enable Purview Content Search, quarantine messages.
|
||||
- Domain-joined workstation compromise -> GuruRMM + Bitdefender workflow (see `clients/ace-portables/reports/` for past example).
|
||||
- Attacker still active and exfiltrating -> consider disabling the user via the `remediate` subcommand and rotating the mailbox password at the same time.
|
||||
127
.claude/skills/remediation-tool/references/gotchas.md
Normal file
127
.claude/skills/remediation-tool/references/gotchas.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Gotchas — Permissions, Roles, Consent
|
||||
|
||||
## Onboarding a new tenant
|
||||
|
||||
Run `onboard-tenant.sh` after admin consents each app. This assigns required directory roles automatically.
|
||||
|
||||
Quick steps:
|
||||
1. Send consent URLs (Tenant Admin FIRST, then others)
|
||||
2. After admin accepts: `bash scripts/onboard-tenant.sh <domain>`
|
||||
3. Verify output shows all roles [OK]
|
||||
4. Update tenant table below
|
||||
|
||||
If Tenant Admin is not yet consented, onboard-tenant.sh will output all needed consent URLs.
|
||||
|
||||
## App Suite (tiered architecture)
|
||||
|
||||
Five multi-tenant apps replace the old single over-permissioned app. Use minimum necessary tier.
|
||||
|
||||
| Tier | Display name in customer tenant | App ID | Vault file |
|
||||
|---|---|---|---|
|
||||
| `investigator` / `investigator-exo` | ComputerGuru Security Investigator | `bfbc12a4-f0dd-4e12-b06d-997e7271e10c` | `computerguru-security-investigator.sops.yaml` |
|
||||
| `exchange-op` | ComputerGuru Exchange Operator | `b43e7342-5b4b-492f-890f-bb5a4f7f40e9` | `computerguru-exchange-operator.sops.yaml` |
|
||||
| `user-manager` | ComputerGuru User Manager | `64fac46b-8b44-41ad-93ee-7da03927576c` | `computerguru-user-manager.sops.yaml` |
|
||||
| `tenant-admin` | ComputerGuru Tenant Admin | `709e6eed-0711-4875-9c44-2d3518c47063` | `computerguru-tenant-admin.sops.yaml` |
|
||||
| `defender` | ComputerGuru Defender Add-on | `dbf8ad1a-54f4-4bb8-8a9e-ea5b9634635b` | `computerguru-defender-addon.sops.yaml` |
|
||||
|
||||
**Deprecated (do not use):** ~~ComputerGuru - AI Remediation~~ (`fabb3421`) — old single-app with 159 permissions including Defender ATP. Broke consent on tenants without MDE license. Retire/delete from portal when confirmed no active tenants depend on it.
|
||||
|
||||
When searching customer admin portals for a service principal (role assignments, app role assignments, CA exclusions), search by the display name for that tier (e.g., "ComputerGuru Security Investigator").
|
||||
|
||||
## Per-tenant prerequisites
|
||||
|
||||
Graph API permissions alone are not enough. Most privileged operations require directory roles on the specific service principal *in that tenant*:
|
||||
|
||||
| Operation | App tier | Required directory role on that SP |
|
||||
|---|---|---|
|
||||
| Exchange REST read (Get-InboxRule, Get-Mailbox) | `investigator-exo` | Exchange Administrator |
|
||||
| Exchange REST write (Set-Mailbox, Remove-InboxRule) | `exchange-op` | Exchange Administrator |
|
||||
| Password reset, user property updates | `user-manager` | User Administrator |
|
||||
| MFA method reset | `user-manager` | Authentication Administrator |
|
||||
| Conditional Access reads/writes | `tenant-admin` | Conditional Access Administrator OR Security Administrator |
|
||||
| Teams policies | `tenant-admin` | Teams Administrator |
|
||||
|
||||
### How to assign a role to an SP in a customer tenant
|
||||
|
||||
1. Sign into the customer's Entra admin center as Global Admin:
|
||||
`https://entra.microsoft.com/#@{customer-domain}`
|
||||
2. Identity -> Roles & admins -> All roles -> select the role (e.g., Exchange Administrator)
|
||||
3. Add assignments -> search by the app display name (e.g., "ComputerGuru Security Investigator") -> Assign
|
||||
(Active, permanent — service principals cannot activate eligible assignments)
|
||||
|
||||
## Admin consent URLs
|
||||
|
||||
Each app must be individually consented in each customer tenant. Format:
|
||||
|
||||
```
|
||||
https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id={app-id}&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
**Security Investigator** (consent this first — needed for all breach checks):
|
||||
```
|
||||
https://login.microsoftonline.com/{TENANT_ID}/adminconsent?client_id=bfbc12a4-f0dd-4e12-b06d-997e7271e10c&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
**Exchange Operator** (consent when remediation scope is needed):
|
||||
```
|
||||
https://login.microsoftonline.com/{TENANT_ID}/adminconsent?client_id=b43e7342-5b4b-492f-890f-bb5a4f7f40e9&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
**User Manager**:
|
||||
```
|
||||
https://login.microsoftonline.com/{TENANT_ID}/adminconsent?client_id=64fac46b-8b44-41ad-93ee-7da03927576c&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
**Tenant Admin**:
|
||||
```
|
||||
https://login.microsoftonline.com/{TENANT_ID}/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
**Defender Add-on** (MDE-licensed tenants only — AADSTS650052 if no MDE license):
|
||||
```
|
||||
https://login.microsoftonline.com/{TENANT_ID}/adminconsent?client_id=dbf8ad1a-54f4-4bb8-8a9e-ea5b9634635b&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
The customer admin signs in as Global Admin, clicks Accept. Redirect lands on azcomputerguru.com — expected. Verify via `/servicePrincipals/{sp-id}/appRoleAssignments` (grants timestamped today confirm success).
|
||||
|
||||
## Diagnosing "required scopes are missing"
|
||||
|
||||
Token returned 403 with `"required scopes are missing in the token"`:
|
||||
|
||||
1. Decode the JWT payload (2nd segment, base64url) and check the `roles` claim.
|
||||
2. If the expected scope is missing from `roles`:
|
||||
- Confirm the scope is in the app manifest in the home tenant (saved, not just selected).
|
||||
- Grant admin consent in the home tenant.
|
||||
- Re-run the customer admin consent URL above for that specific app.
|
||||
3. If the scope IS in `roles` but you still get 403: check for a missing directory role (see table above).
|
||||
|
||||
## Diagnosing Exchange REST 403
|
||||
|
||||
- Wrong token scope: must request `https://outlook.office365.com/.default` (use `investigator-exo` or `exchange-op` tier, NOT `investigator`).
|
||||
- Missing Exchange Administrator role on the specific SP in that tenant.
|
||||
- Propagation delay: newly assigned role can take up to 15 minutes to reach Exchange Online. If just assigned, wait and retry.
|
||||
|
||||
## AADSTS650052 — service not licensed
|
||||
|
||||
If token request or API call returns AADSTS650052 referencing `WindowsDefenderATP` (`fc780465`): the tenant does not have an MDE license. Do not use the `defender` tier for this tenant. Security investigation proceeds with `investigator` + `investigator-exo` only.
|
||||
|
||||
## Common, benign "failures" in sign-in logs
|
||||
|
||||
- `error 50140` "Keep me signed in interrupt" — KMSI prompt, not a real failure.
|
||||
- `error 65001` "has not consented to use the application" — fires during onboarding and before consent granted. If `appDisplayName` matches any ComputerGuru app, those are our own consent attempts, not attacker activity.
|
||||
- `error 50126` from the sysadmin account during onboarding is typo/retry noise — check `ipAddress` against Mike's known IPs before flagging.
|
||||
|
||||
## Tenants where apps are consented (as of 2026-04-20)
|
||||
|
||||
| Tenant | Tenant ID | Sec Inv | Exch Op | User Mgr | Tenant Admin | Defender | Exch Admin (Sec Inv) | User Admin (User Mgr) | Auth Admin (User Mgr) | Notes |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Valleywide Plastering | 5c53ae9f... | old app only | — | — | — | — | — | old app only | — | Needs migration to new app suite |
|
||||
| Dataforth | 7dfa3ce8... | old app only | — | — | — | — | old app only | old app only | — | Needs migration |
|
||||
| Cascades Tucson | 207fa277-e9d8-4eb7-ada1-1064d2221498 | old app only | — | — | — | — | old app only | old app only | — | IdentityRiskyUser scope still not consented as of 2026-04-16 |
|
||||
| Grabblaw | 032b383e-96e4-491b-880d-3fd3295672c3 | YES (2026-04-20) | — | YES (2026-04-20) | YES (2026-04-20) | — | ASSIGNED (2026-04-20) | ASSIGNED (2026-04-20) | ASSIGNED (2026-04-20) | Fully onboarded |
|
||||
| martylryan.com | (resolve via script) | YES (2026-04-20) | — | YES (old app) | YES (2026-04-20) | — | ASSIGNED (2026-04-20) | ASSIGNED (2026-04-20) | ASSIGNED (2026-04-20) | Fully onboarded |
|
||||
| mvaninc.com | 5affaf1e-de89-416b-a655-1b2cf615d5b1 | YES (2026-04-21) | — | YES (2026-04-21) | YES (2026-04-21) | — | — | — | — | Fully onboarded. Incident 2026-04-21: sysadmin GA account unauthorized sign-in from OKC via device PRT (MITCH-LAPTOP/JUNE). Remediated: pw reset, sessions revoked. CA policy (MFA all users) still pending — Mike to create. |
|
||||
|
||||
**Migration note:** Valleywide, Dataforth, and Cascades still use the old deprecated app. Next visit: consent Security Investigator + assign Exchange Administrator role to new SP, then retire old app consent.
|
||||
|
||||
Keep this table updated when rolling out to new tenants or migrating existing ones. Run `onboard-tenant.sh` after each consent and update the role columns from the script's final status output.
|
||||
159
.claude/skills/remediation-tool/references/graph-endpoints.md
Normal file
159
.claude/skills/remediation-tool/references/graph-endpoints.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# Graph + Exchange REST Cheatsheet
|
||||
|
||||
All examples assume:
|
||||
- `$GT` = Graph token (`investigator` tier)
|
||||
- `$EXO_R` = Exchange read token (`investigator-exo` tier) — Get-* cmdlets
|
||||
- `$EXO_W` = Exchange write token (`exchange-op` tier) — Set-*/Remove-* cmdlets
|
||||
- `$UT` = User Manager graph token (`user-manager` tier) — user write ops
|
||||
- `$TID` = tenant ID, `$UPN`/`$UID` = user identifiers
|
||||
|
||||
Acquire tokens:
|
||||
```bash
|
||||
GT=$(bash .claude/skills/remediation-tool/scripts/get-token.sh $TID investigator)
|
||||
EXO_R=$(bash .claude/skills/remediation-tool/scripts/get-token.sh $TID investigator-exo)
|
||||
EXO_W=$(bash .claude/skills/remediation-tool/scripts/get-token.sh $TID exchange-op) # remediation only
|
||||
UT=$(bash .claude/skills/remediation-tool/scripts/get-token.sh $TID user-manager) # remediation only
|
||||
```
|
||||
|
||||
## Graph API (`https://graph.microsoft.com/v1.0`)
|
||||
|
||||
### User lookup / status
|
||||
|
||||
```bash
|
||||
# By UPN
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users/$UPN?\$select=id,displayName,userPrincipalName,mail,accountEnabled,createdDateTime,lastPasswordChangeDateTime"
|
||||
|
||||
# All users (filter, paged)
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users?\$top=999&\$filter=accountEnabled%20eq%20true"
|
||||
```
|
||||
|
||||
### Mailbox
|
||||
|
||||
```bash
|
||||
# Visible inbox rules (Graph v1.0 — does NOT return hidden rules)
|
||||
/users/$UPN/mailFolders/inbox/messageRules
|
||||
|
||||
# Mailbox settings (auto-reply, delegates meeting option, NOT forwarding flags)
|
||||
/users/$UPN/mailboxSettings
|
||||
|
||||
# Recent sent / deleted
|
||||
/users/$UPN/mailFolders/sentitems/messages?$top=25&$orderby=sentDateTime%20desc
|
||||
/users/$UPN/mailFolders/deleteditems/messages?$top=25&$orderby=receivedDateTime%20desc
|
||||
```
|
||||
|
||||
### Authentication methods
|
||||
|
||||
```bash
|
||||
/users/$UPN/authentication/methods
|
||||
# Watch for new methods added within the attack window
|
||||
```
|
||||
|
||||
### OAuth + app role assignments
|
||||
|
||||
```bash
|
||||
/users/$UPN/oauth2PermissionGrants # user-level consents
|
||||
/users/$UPN/appRoleAssignments # apps assigned to this user
|
||||
/servicePrincipals/$SP_ID/appRoleAssignments # what scopes a SP has
|
||||
```
|
||||
|
||||
### Sign-ins (needs Entra ID P1 or higher)
|
||||
|
||||
```bash
|
||||
# Interactive sign-ins v1.0 (does NOT include non-interactive/service-principal)
|
||||
/auditLogs/signIns?$filter=userId eq '$UID' and createdDateTime ge $FROM&$top=200
|
||||
|
||||
# All sign-in event types (beta endpoint)
|
||||
/beta/auditLogs/signIns?$filter=userId eq '$UID' and (signInEventTypes/any(t:t eq 'nonInteractiveUser'))
|
||||
|
||||
# Foreign successful sign-ins tenant-wide
|
||||
/auditLogs/signIns?$filter=(status/errorCode eq 0) and (location/countryOrRegion ne 'US')
|
||||
```
|
||||
|
||||
### Directory audits
|
||||
|
||||
```bash
|
||||
# Changes targeting a specific user
|
||||
/auditLogs/directoryAudits?$filter=targetResources/any(t:t/id eq '$UID')
|
||||
|
||||
# Tenant-wide consent / auth-method / role events
|
||||
/auditLogs/directoryAudits?$filter=activityDateTime ge $FROM
|
||||
# Then client-side filter by activityDisplayName ~ Consent|Authentication Method|Add service principal|Add member to role
|
||||
```
|
||||
|
||||
### Identity Protection (needs IdentityRiskyUser.Read.All)
|
||||
|
||||
```bash
|
||||
/identityProtection/riskyUsers
|
||||
/identityProtection/riskyUsers/$UID
|
||||
/identityProtection/riskDetections?$filter=userId eq '$UID'
|
||||
```
|
||||
|
||||
### B2B guests
|
||||
|
||||
```bash
|
||||
# Get guest by gmail/external address
|
||||
/users?$filter=startswith(userPrincipalName,'dunedolly21')
|
||||
|
||||
# Invite audits
|
||||
/auditLogs/directoryAudits?$filter=activityDisplayName eq 'Invite external user'
|
||||
```
|
||||
|
||||
## Exchange Online REST (`https://outlook.office365.com/adminapi/beta/{tenant-id}/InvokeCommand`)
|
||||
|
||||
POST with JSON body `{"CmdletInput":{"CmdletName":"<cmdlet>","Parameters":{...}}}`.
|
||||
- **Read ops** (Get-*): use `$EXO_R` — Security Investigator token (`investigator-exo` tier)
|
||||
- **Write ops** (Set-*, Remove-*): use `$EXO_W` — Exchange Operator token (`exchange-op` tier)
|
||||
|
||||
### Inbox rules (INCLUDING hidden)
|
||||
|
||||
```json
|
||||
{"CmdletInput":{"CmdletName":"Get-InboxRule","Parameters":{"Mailbox":"user@domain.com","IncludeHidden":true}}}
|
||||
```
|
||||
|
||||
Why this matters: attackers commonly create hidden rules that Graph v1.0 cannot see.
|
||||
|
||||
### Mailbox forwarding / properties
|
||||
|
||||
```json
|
||||
{"CmdletInput":{"CmdletName":"Get-Mailbox","Parameters":{"Identity":"user@domain.com"}}}
|
||||
```
|
||||
|
||||
Check: `ForwardingAddress`, `ForwardingSmtpAddress`, `DeliverToMailboxAndForward`, `GrantSendOnBehalfTo`, `HiddenFromAddressListsEnabled`.
|
||||
|
||||
### Mailbox permissions (delegates / FullAccess)
|
||||
|
||||
```json
|
||||
{"CmdletInput":{"CmdletName":"Get-MailboxPermission","Parameters":{"Identity":"user@domain.com"}}}
|
||||
```
|
||||
|
||||
Filter out `NT AUTHORITY\\SELF` — anything else is a delegate.
|
||||
|
||||
### SendAs permissions
|
||||
|
||||
```json
|
||||
{"CmdletInput":{"CmdletName":"Get-RecipientPermission","Parameters":{"Identity":"user@domain.com"}}}
|
||||
```
|
||||
|
||||
### Transport rules (tenant-wide mail flow)
|
||||
|
||||
```json
|
||||
{"CmdletInput":{"CmdletName":"Get-TransportRule","Parameters":{}}}
|
||||
```
|
||||
|
||||
Check for rules that reroute, delete, or exfiltrate mail.
|
||||
|
||||
### SMTP AUTH
|
||||
|
||||
```json
|
||||
{"CmdletInput":{"CmdletName":"Get-CASMailbox","Parameters":{"Identity":"user@domain.com"}}}
|
||||
```
|
||||
|
||||
Check `SmtpClientAuthenticationDisabled`. To disable SMTP AUTH on a single mailbox (remediation): `Set-CASMailbox -SmtpClientAuthenticationDisabled $true`.
|
||||
|
||||
## Rate limits / pagination
|
||||
|
||||
- Graph signIns endpoints cap `$top` at 999. For >999 results, follow `@odata.nextLink`.
|
||||
- Exchange REST has undocumented throttling — if you hit 429, back off 30–60s.
|
||||
- Token is valid ~60 minutes. Script caches for 55 min.
|
||||
197
.claude/skills/remediation-tool/references/tenant-consent.html
Normal file
197
.claude/skills/remediation-tool/references/tenant-consent.html
Normal file
@@ -0,0 +1,197 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ComputerGuru — Tenant Admin Consent</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f1117; color: #e2e8f0; min-height: 100vh; padding: 32px 24px; }
|
||||
h1 { font-size: 1.4rem; font-weight: 600; color: #f8fafc; margin-bottom: 4px; }
|
||||
.subtitle { font-size: 0.85rem; color: #64748b; margin-bottom: 28px; }
|
||||
.section-label { font-size: 0.7rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: #475569; margin-bottom: 10px; margin-top: 24px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 8px; }
|
||||
.card { background: #1e2330; border: 1px solid #2d3548; border-radius: 8px; padding: 14px 16px; display: flex; align-items: center; justify-content: space-between; gap: 12px; transition: border-color 0.15s; }
|
||||
.card:hover { border-color: #3b82f6; }
|
||||
.card.done { border-color: #166534; background: #14281f; opacity: 0.7; }
|
||||
.card.reconsent { border-color: #92400e; background: #1c1a0f; }
|
||||
.tenant-info { min-width: 0; }
|
||||
.tenant-name { font-size: 0.9rem; font-weight: 500; color: #e2e8f0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.tenant-domain { font-size: 0.75rem; color: #64748b; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.btn { flex-shrink: 0; font-size: 0.78rem; font-weight: 500; padding: 6px 14px; border-radius: 5px; border: none; cursor: pointer; text-decoration: none; display: inline-block; white-space: nowrap; transition: background 0.15s; }
|
||||
.btn-primary { background: #2563eb; color: #fff; }
|
||||
.btn-primary:hover { background: #1d4ed8; }
|
||||
.btn-warn { background: #92400e; color: #fef3c7; }
|
||||
.btn-warn:hover { background: #b45309; }
|
||||
.btn-done { background: #166534; color: #bbf7d0; cursor: default; }
|
||||
.badge { font-size: 0.65rem; font-weight: 600; padding: 2px 7px; border-radius: 3px; display: inline-block; margin-top: 3px; }
|
||||
.badge-reconsent { background: #451a03; color: #fbbf24; }
|
||||
.badge-done { background: #052e16; color: #4ade80; }
|
||||
.stats { display: flex; gap: 20px; margin-bottom: 24px; padding: 16px; background: #1e2330; border: 1px solid #2d3548; border-radius: 8px; }
|
||||
.stat-val { font-size: 1.4rem; font-weight: 700; }
|
||||
.stat-label { font-size: 0.72rem; color: #64748b; margin-top: 1px; }
|
||||
.stat-pending .stat-val { color: #f59e0b; }
|
||||
.stat-done .stat-val { color: #4ade80; }
|
||||
.stat-reconsent .stat-val { color: #fb923c; }
|
||||
.instructions { background: #1e2330; border: 1px solid #2d3548; border-left: 3px solid #3b82f6; border-radius: 8px; padding: 14px 16px; margin-bottom: 24px; font-size: 0.82rem; color: #94a3b8; line-height: 1.6; }
|
||||
.instructions strong { color: #e2e8f0; }
|
||||
code { background: #0f1117; padding: 2px 6px; border-radius: 3px; font-family: monospace; font-size: 0.8em; color: #93c5fd; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>ComputerGuru — Tenant Admin Consent</h1>
|
||||
<p class="subtitle">Send the consent URL to each customer's Global Admin. After they accept, run <code>onboard-tenant.sh <domain></code>.</p>
|
||||
|
||||
<div class="instructions">
|
||||
<strong>One-click onboarding flow:</strong> Customer Global Admin clicks their consent link below → logs in → clicks Accept.
|
||||
Then run: <code>bash scripts/onboard-tenant.sh <domain></code> — the script will automatically consent all other apps and assign all directory roles.
|
||||
<br><br>
|
||||
<strong>Re-consent</strong> (orange) means Tenant Admin was previously consented but needs a refresh to pick up <code>AppRoleAssignment.ReadWrite.All</code>.
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat stat-pending"><div class="stat-val" id="cnt-pending">—</div><div class="stat-label">Pending consent</div></div>
|
||||
<div class="stat stat-reconsent"><div class="stat-val" id="cnt-reconsent">—</div><div class="stat-label">Need re-consent</div></div>
|
||||
<div class="stat stat-done"><div class="stat-val" id="cnt-done">—</div><div class="stat-label">Done</div></div>
|
||||
</div>
|
||||
|
||||
<div class="section-label">Re-consent required</div>
|
||||
<div class="grid" id="grid-reconsent"></div>
|
||||
|
||||
<div class="section-label">Pending initial consent</div>
|
||||
<div class="grid" id="grid-pending"></div>
|
||||
|
||||
<div class="section-label" id="label-done" style="display:none">Fully onboarded</div>
|
||||
<div class="grid" id="grid-done"></div>
|
||||
|
||||
<script>
|
||||
const BASE = "https://login.microsoftonline.com";
|
||||
const CLIENT = "709e6eed-0711-4875-9c44-2d3518c47063";
|
||||
const REDIRECT = "https://azcomputerguru.com";
|
||||
|
||||
const TENANTS = [
|
||||
// status: "done" | "reconsent" | "pending"
|
||||
{ name: "Marty Ryan", domain: "martylryan.com", id: "48581923-2153-48b9-82b3-6a3587813041", status: "done" },
|
||||
{ name: "Grabblaw", domain: "grabblaw.com", id: "032b383e-96e4-491b-880d-3fd3295672c3", status: "done" },
|
||||
{ name: "Andy's Mobile Fuel", domain: "andysmobilefuel.com", id: "806d4728-4545-495e-9eba-f0f96584ea08", status: "done" },
|
||||
{ name: "Bill Tedards", domain: "tedards.net", id: "4fcbb1f4-fbf9-4548-a93e-7d14a3c091e6", status: "done" },
|
||||
{ name: "Brian Kahn", domain: "lIGQB0q47JGi8MGBPBAmzBfDHdf.onmicrosoft.com", id: "f5f86b40-4345-406e-94a3-470376d7590b", status: "pending" },
|
||||
{ name: "cascadestucson.com", domain: "cascadestucson.com", id: "207fa277-e9d8-4eb7-ada1-1064d2221498", status: "done" },
|
||||
{ name: "cclac.net", domain: "cclac.net", id: "e8a0fafc-21ee-41e8-a5ba-f3a250a8a30e", status: "done" },
|
||||
{ name: "Cobalt Fine Arts", domain: "cobaltfinearts.com", id: "03c4d4ec-b6d3-4061-a75c-8a4250ba2b29", status: "done" },
|
||||
{ name: "CUADRO LLC", domain: "cuadro.design", id: "b68c7171-31d6-4b63-8243-7a2cade9caf8", status: "pending" },
|
||||
{ name: "Curtis Plumbing", domain: "cparizona.onmicrosoft.com", id: "d2d7ea54-9146-42d1-b99e-0da098550bde", status: "pending" },
|
||||
{ name: "cwconcretellc.com", domain: "NETORGFT11452752.onmicrosoft.com", id: "dfee2224-93cd-4291-9b09-6c6ce9bb8711", status: "pending" },
|
||||
{ name: "Dataforth Corporation", domain: "dataforth.com", id: "7dfa3ce8-c496-4b51-ab8d-bd3dcd78b584", status: "done" },
|
||||
{ name: "Feline Ltd Cat Clinic", domain: "felineltd.onmicrosoft.com", id: "1b5f38ef-b6c8-4b6d-9bfb-9250ea7e7994", status: "pending" },
|
||||
{ name: "Glaz-Tech Industries", domain: "glaztech.com", id: "82931e3c-de7a-4f74-87f7-fe714be1f160", status: "done" },
|
||||
{ name: "Heieck Sheila", domain: "heieck.org", id: "7462ce7e-071e-49da-88ec-50ec6b46d12e", status: "done" },
|
||||
{ name: "ICE INC", domain: "iceinc.us.com", id: "ff26952e-970d-4c02-9179-416ed931ec50", status: "pending" },
|
||||
{ name: "Instrumental Music Ctr", domain: "instrumentalmusic.onmicrosoft.com", id: "65adab75-f1fd-4ef9-b2b4-c24f595393e3", status: "pending" },
|
||||
{ name: "Jema Enterprises, LLC", domain: "jemaenterprises.com", id: "41268042-9a8e-41c2-9a3c-0775398b86cb", status: "done" },
|
||||
{ name: "JR Kennedy Company", domain: "jrkco.com", id: "a92594b9-c8ad-4dba-8b40-14fcd32c723c", status: "pending" },
|
||||
{ name: "Khalsa Montessori", domain: "khalsamontessorischools.onmicrosoft.com", id: "b2950f9d-81f8-40e4-85d9-2854d1d4f31b", status: "pending" },
|
||||
{ name: "Kittle Design & Const.", domain: "kittlearizona.com", id: "3d073ebe-806a-4a5e-9035-3c7c4a264fc0", status: "pending" },
|
||||
{ name: "LeeAnn Parkinson", domain: "lamaddux.com", id: "2f0c4c92-c608-4ee0-bdc2-87d5fd8fe929", status: "pending" },
|
||||
{ name: "MVAN Enterprises", domain: "mvan.onmicrosoft.com", id: "5affaf1e-de89-416b-a655-1b2cf615d5b1", status: "done" },
|
||||
{ name: "Patient Care Advocates", domain: "pcatucson.com", id: "463b462d-0995-4e51-9e41-82c208015c7f", status: "pending" },
|
||||
{ name: "Peaceful Spirit Massage", domain: "bestmassageintucson.com", id: "13be285a-374d-4a7c-a7d8-4cb5a98b5c29", status: "done" },
|
||||
{ name: "Putt Land Surveying", domain: "puttsurveying.com", id: "25008634-91b4-40aa-8113-78ea03826156", status: "pending" },
|
||||
{ name: "Rednour Law", domain: "rednourlaw.com", id: "4a4ca18a-f516-478b-99da-2e0722c5dc18", status: "done" },
|
||||
{ name: "Reliant Well Drilling", domain: "reliantpump.services", id: "2b124552-3891-4090-b3ed-2eebad3c4083", status: "done" },
|
||||
{ name: "Ridgetop Group", domain: "ridgetopgroup.com", id: "ef111bfc-9c90-43c9-a581-f9bbfceb6517", status: "done" },
|
||||
{ name: "Rincon Vista Vet Ctr", domain: "rinconvistavet.onmicrosoft.com", id: "b8cdcd89-d0f4-4747-bcf3-8bd8a25fd7e1", status: "pending" },
|
||||
{ name: "Russo Law Firm", domain: "rrs-law.com", id: "bef1b190-f78f-4b1c-aa4b-fab186a30702", status: "pending" },
|
||||
{ name: "Safe Site Utility Svcs", domain: "safesitellc.com", id: "71b4e637-c802-4137-a812-ae50dbc839e3", status: "done" },
|
||||
{ name: "SANDTEKO MACHINERY", domain: "SANDTEKOMACHINERY.com", id: "739bb777-cf76-478f-866b-f61c830c8246", status: "pending" },
|
||||
{ name: "Shave, Kevin", domain: "az2son.com", id: "984c05a9-708b-4ec1-9f43-558865cb3c9d", status: "pending" },
|
||||
{ name: "Sonorangreenllc.com", domain: "sonorangreenllc.com", id: "ededa4fb-f6eb-4398-851d-5eb3e11fab27", status: "done" },
|
||||
{ name: "Starr Pass Realty", domain: "starrpass.com", id: "222450dd-141f-435f-87b8-cec719aac99e", status: "pending" },
|
||||
{ name: "The Dumpster Guys", domain: "dumpsterguys.onmicrosoft.com", id: "0b3cd451-2679-4697-b161-07b9ef8d41e9", status: "pending" },
|
||||
{ name: "The Prairie Schooner", domain: "theprairieschooner.onmicrosoft.com", id: "c941033c-2752-42ef-be22-fbab77e2e587", status: "pending" },
|
||||
{ name: "Tucson Golden Corral", domain: "tucsongoldencorral.onmicrosoft.com", id: "50e23e94-960f-4f61-8a27-97dbbe001a36", status: "pending" },
|
||||
{ name: "Tucson Mountain Motors", domain: "tucsonmountainmotors.com", id: "ffdabd05-236b-4666-a7f5-cc40ae9f9122", status: "pending" },
|
||||
{ name: "Valley Wide Plastering", domain: "valleywideplastering.com", id: "5c53ae9f-7071-4248-b834-8685b646450f", status: "done" },
|
||||
{ name: "Von's Carstar", domain: "vonscarstar.com", id: "53de51b9-a063-4f46-88ff-7c3468828ed9", status: "pending" },
|
||||
];
|
||||
|
||||
// Load done state from localStorage
|
||||
const DONE_KEY = "cg_consent_done";
|
||||
const done = new Set(JSON.parse(localStorage.getItem(DONE_KEY) || "[]"));
|
||||
|
||||
function consentUrl(id) {
|
||||
return `${BASE}/${id}/adminconsent?client_id=${CLIENT}&redirect_uri=${REDIRECT}&prompt=consent`;
|
||||
}
|
||||
|
||||
function markDone(id, btn, card) {
|
||||
done.add(id);
|
||||
localStorage.setItem(DONE_KEY, JSON.stringify([...done]));
|
||||
btn.textContent = "Done";
|
||||
btn.className = "btn btn-done";
|
||||
btn.onclick = null;
|
||||
card.className = "card done";
|
||||
updateCounts();
|
||||
}
|
||||
|
||||
function renderCard(t) {
|
||||
const isDone = done.has(t.id);
|
||||
const card = document.createElement("div");
|
||||
card.className = "card" + (isDone ? " done" : t.status === "reconsent" ? " reconsent" : "");
|
||||
|
||||
const info = document.createElement("div");
|
||||
info.className = "tenant-info";
|
||||
info.innerHTML = `<div class="tenant-name">${t.name}</div><div class="tenant-domain">${t.domain}</div>` +
|
||||
(t.status === "reconsent" && !isDone ? `<span class="badge badge-reconsent">Re-consent</span>` : "") +
|
||||
(isDone ? `<span class="badge badge-done">Done</span>` : "");
|
||||
|
||||
const btn = document.createElement("a");
|
||||
btn.href = consentUrl(t.id);
|
||||
btn.target = "_blank";
|
||||
btn.rel = "noopener";
|
||||
|
||||
if (isDone) {
|
||||
btn.textContent = "Done";
|
||||
btn.className = "btn btn-done";
|
||||
btn.onclick = (e) => e.preventDefault();
|
||||
} else if (t.status === "reconsent") {
|
||||
btn.textContent = "Re-consent";
|
||||
btn.className = "btn btn-warn";
|
||||
btn.onclick = () => setTimeout(() => markDone(t.id, btn, card), 500);
|
||||
} else {
|
||||
btn.textContent = "Consent";
|
||||
btn.className = "btn btn-primary";
|
||||
btn.onclick = () => setTimeout(() => markDone(t.id, btn, card), 500);
|
||||
}
|
||||
|
||||
card.appendChild(info);
|
||||
card.appendChild(btn);
|
||||
return { card, isDone };
|
||||
}
|
||||
|
||||
function updateCounts() {
|
||||
const total = TENANTS.length;
|
||||
const doneCount = TENANTS.filter(t => done.has(t.id)).length;
|
||||
const reconsent = TENANTS.filter(t => t.status === "reconsent" && !done.has(t.id)).length;
|
||||
const pending = TENANTS.filter(t => t.status === "pending" && !done.has(t.id)).length;
|
||||
document.getElementById("cnt-pending").textContent = pending;
|
||||
document.getElementById("cnt-reconsent").textContent = reconsent;
|
||||
document.getElementById("cnt-done").textContent = doneCount;
|
||||
const labelDone = document.getElementById("label-done");
|
||||
labelDone.style.display = doneCount > 0 ? "" : "none";
|
||||
}
|
||||
|
||||
function render() {
|
||||
const grids = { pending: document.getElementById("grid-pending"), reconsent: document.getElementById("grid-reconsent"), done: document.getElementById("grid-done") };
|
||||
TENANTS.forEach(t => {
|
||||
const { card, isDone } = renderCard(t);
|
||||
if (isDone) grids.done.appendChild(card);
|
||||
else grids[t.status].appendChild(card);
|
||||
});
|
||||
updateCounts();
|
||||
}
|
||||
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
185
.claude/skills/remediation-tool/references/tenants.md
Normal file
185
.claude/skills/remediation-tool/references/tenants.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# Managed Tenant Reference
|
||||
|
||||
Last updated: 2026-04-20. Source of truth: CIPP ListTenants API.
|
||||
|
||||
Run `bash scripts/onboard-tenant.sh <domain>` after any tenant consents Tenant Admin.
|
||||
After full onboarding, update the Onboarded column below.
|
||||
|
||||
## Tenant List
|
||||
|
||||
| Display Name | Domain | Tenant ID | Onboarded | Notes |
|
||||
|---|---|---|---|---|
|
||||
| Andy's Mobile Fuel | andysmobilefuel.com | 806d4728-4545-495e-9eba-f0f96584ea08 | NO | |
|
||||
| Bill Tedards | tedards.net | 4fcbb1f4-fbf9-4548-a93e-7d14a3c091e6 | NO | |
|
||||
| Brian Kahn | lIGQB0q47JGi8MGBPBAmzBfDHdf.onmicrosoft.com | f5f86b40-4345-406e-94a3-470376d7590b | NO | |
|
||||
| cascadestucson.com | cascadestucson.com | 207fa277-e9d8-4eb7-ada1-1064d2221498 | NO | Old app only; IdentityRiskyUser not consented |
|
||||
| cclac.net | cclac.net | e8a0fafc-21ee-41e8-a5ba-f3a250a8a30e | NO | |
|
||||
| Cobalt Fine Arts | cobaltfinearts.com | 03c4d4ec-b6d3-4061-a75c-8a4250ba2b29 | NO | |
|
||||
| CUADRO LLC | cuadro.design | b68c7171-31d6-4b63-8243-7a2cade9caf8 | NO | |
|
||||
| Curtis Plumbing | cparizona.onmicrosoft.com | d2d7ea54-9146-42d1-b99e-0da098550bde | NO | |
|
||||
| cwconcretellc.com | NETORGFT11452752.onmicrosoft.com | dfee2224-93cd-4291-9b09-6c6ce9bb8711 | NO | |
|
||||
| Dataforth Corporation | dataforth.com | 7dfa3ce8-c496-4b51-ab8d-bd3dcd78b584 | YES | All apps consented; Sec Inv + Exch Op + User Mgr roles assigned 2026-04-23; Exch Op Exchange Admin role added manually |
|
||||
| Feline Limited Cat Clinic | felineltd.onmicrosoft.com | 1b5f38ef-b6c8-4b6d-9bfb-9250ea7e7994 | NO | |
|
||||
| Glaz-Tech Industries | glaztech.com | 82931e3c-de7a-4f74-87f7-fe714be1f160 | NO | |
|
||||
| Grabblaw | grabblaw.com | 032b383e-96e4-491b-880d-3fd3295672c3 | YES | Sec Inv + User Mgr + Tenant Admin consented; all roles assigned 2026-04-20 |
|
||||
| Heieck Sheila | heieck.org | 7462ce7e-071e-49da-88ec-50ec6b46d12e | NO | |
|
||||
| ICE INC | iceinc.us.com | ff26952e-970d-4c02-9179-416ed931ec50 | NO | |
|
||||
| Instrumental Music Center | instrumentalmusic.onmicrosoft.com | 65adab75-f1fd-4ef9-b2b4-c24f595393e3 | NO | |
|
||||
| Jema Enterprises, LLC | jemaenterprises.com | 41268042-9a8e-41c2-9a3c-0775398b86cb | NO | |
|
||||
| JR Kennedy Company | jrkco.com | a92594b9-c8ad-4dba-8b40-14fcd32c723c | NO | |
|
||||
| Khalsa Montessori School | khalsamontessorischools.onmicrosoft.com | b2950f9d-81f8-40e4-85d9-2854d1d4f31b | NO | |
|
||||
| Kittle Design & Construction | kittlearizona.com | 3d073ebe-806a-4a5e-9035-3c7c4a264fc0 | PARTIAL | Sec Inv consented 2026-04-23; Exchange Admin role NOT assigned; Tenant Admin not consented; breach check run — Alexis + Ken inbox rules flagged |
|
||||
| LeeAnn Parkinson | lamaddux.com | 2f0c4c92-c608-4ee0-bdc2-87d5fd8fe929 | NO | |
|
||||
| Marty Ryan | martylryan.com | 48581923-2153-48b9-82b3-6a3587813041 | YES | Sec Inv + Tenant Admin consented; all roles assigned 2026-04-20 |
|
||||
| MVAN Enterprises, Inc | mvan.onmicrosoft.com | 5affaf1e-de89-416b-a655-1b2cf615d5b1 | NO | |
|
||||
| Patient Care Advocates | pcatucson.com | 463b462d-0995-4e51-9e41-82c208015c7f | NO | |
|
||||
| Peaceful Spirit Massage | bestmassageintucson.com | 13be285a-374d-4a7c-a7d8-4cb5a98b5c29 | NO | |
|
||||
| Putt Land Surveying Inc | puttsurveying.com | 25008634-91b4-40aa-8113-78ea03826156 | NO | |
|
||||
| Rednour Law | rednourlaw.com | 4a4ca18a-f516-478b-99da-2e0722c5dc18 | NO | |
|
||||
| Reliant Well Drilling and Pump | reliantpump.services | 2b124552-3891-4090-b3ed-2eebad3c4083 | NO | |
|
||||
| Ridgetop Group | ridgetopgroup.com | ef111bfc-9c90-43c9-a581-f9bbfceb6517 | NO | |
|
||||
| Rincon Vista Veterinary Center | rinconvistavet.onmicrosoft.com | b8cdcd89-d0f4-4747-bcf3-8bd8a25fd7e1 | NO | |
|
||||
| Russo Law Firm | rrs-law.com | bef1b190-f78f-4b1c-aa4b-fab186a30702 | NO | |
|
||||
| Safe Site Utility Services LLC | safesitellc.com | 71b4e637-c802-4137-a812-ae50dbc839e3 | NO | |
|
||||
| SANDTEKO MACHINERY LLC | SANDTEKOMACHINERY.com | 739bb777-cf76-478f-866b-f61c830c8246 | NO | |
|
||||
| Shave, Kevin | az2son.com | 984c05a9-708b-4ec1-9f43-558865cb3c9d | NO | |
|
||||
| Sonorangreenllc.com | sonorangreenllc.com | ededa4fb-f6eb-4398-851d-5eb3e11fab27 | NO | |
|
||||
| Starr Pass Realty | starrpass.com | 222450dd-141f-435f-87b8-cec719aac99e | NO | |
|
||||
| The Dumpster Guys | dumpsterguys.onmicrosoft.com | 0b3cd451-2679-4697-b161-07b9ef8d41e9 | NO | |
|
||||
| The Prairie Schooner | theprairieschooner.onmicrosoft.com | c941033c-2752-42ef-be22-fbab77e2e587 | NO | |
|
||||
| Tucson Golden Corral | tucsongoldencorral.onmicrosoft.com | 50e23e94-960f-4f61-8a27-97dbbe001a36 | NO | |
|
||||
| Tucson Mountain Motors | tucsonmountainmotors.com | ffdabd05-236b-4666-a7f5-cc40ae9f9122 | NO | |
|
||||
| Valley Wide Plastering | valleywideplastering.com | 5c53ae9f-7071-4248-b834-8685b646450f | NO | Old app only |
|
||||
| Von's Carstar | vonscarstar.com | 53de51b9-a063-4f46-88ff-7c3468828ed9 | NO | |
|
||||
|
||||
## Tenant Admin Consent URLs (batch)
|
||||
|
||||
Send this URL to each customer's Global Admin. After they accept, run:
|
||||
`bash scripts/onboard-tenant.sh <domain>`
|
||||
|
||||
Consent URL format:
|
||||
```
|
||||
https://login.microsoftonline.com/{TENANT_ID}/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
### Per-tenant consent URLs
|
||||
|
||||
Andy's Mobile Fuel:
|
||||
https://login.microsoftonline.com/806d4728-4545-495e-9eba-f0f96584ea08/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Bill Tedards:
|
||||
https://login.microsoftonline.com/4fcbb1f4-fbf9-4548-a93e-7d14a3c091e6/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Brian Kahn:
|
||||
https://login.microsoftonline.com/f5f86b40-4345-406e-94a3-470376d7590b/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
cascadestucson.com:
|
||||
https://login.microsoftonline.com/207fa277-e9d8-4eb7-ada1-1064d2221498/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
cclac.net:
|
||||
https://login.microsoftonline.com/e8a0fafc-21ee-41e8-a5ba-f3a250a8a30e/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Cobalt Fine Arts:
|
||||
https://login.microsoftonline.com/03c4d4ec-b6d3-4061-a75c-8a4250ba2b29/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
CUADRO LLC:
|
||||
https://login.microsoftonline.com/b68c7171-31d6-4b63-8243-7a2cade9caf8/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Curtis Plumbing:
|
||||
https://login.microsoftonline.com/d2d7ea54-9146-42d1-b99e-0da098550bde/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
cwconcretellc.com:
|
||||
https://login.microsoftonline.com/dfee2224-93cd-4291-9b09-6c6ce9bb8711/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Dataforth Corporation:
|
||||
https://login.microsoftonline.com/7dfa3ce8-c496-4b51-ab8d-bd3dcd78b584/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Feline Limited Cat Clinic:
|
||||
https://login.microsoftonline.com/1b5f38ef-b6c8-4b6d-9bfb-9250ea7e7994/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Glaz-Tech Industries:
|
||||
https://login.microsoftonline.com/82931e3c-de7a-4f74-87f7-fe714be1f160/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Heieck Sheila:
|
||||
https://login.microsoftonline.com/7462ce7e-071e-49da-88ec-50ec6b46d12e/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
ICE INC:
|
||||
https://login.microsoftonline.com/ff26952e-970d-4c02-9179-416ed931ec50/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Instrumental Music Center:
|
||||
https://login.microsoftonline.com/65adab75-f1fd-4ef9-b2b4-c24f595393e3/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Jema Enterprises, LLC:
|
||||
https://login.microsoftonline.com/41268042-9a8e-41c2-9a3c-0775398b86cb/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
JR Kennedy Company:
|
||||
https://login.microsoftonline.com/a92594b9-c8ad-4dba-8b40-14fcd32c723c/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Khalsa Montessori School:
|
||||
https://login.microsoftonline.com/b2950f9d-81f8-40e4-85d9-2854d1d4f31b/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Kittle Design & Construction:
|
||||
https://login.microsoftonline.com/3d073ebe-806a-4a5e-9035-3c7c4a264fc0/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
LeeAnn Parkinson:
|
||||
https://login.microsoftonline.com/2f0c4c92-c608-4ee0-bdc2-87d5fd8fe929/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
MVAN Enterprises, Inc:
|
||||
https://login.microsoftonline.com/5affaf1e-de89-416b-a655-1b2cf615d5b1/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Patient Care Advocates:
|
||||
https://login.microsoftonline.com/463b462d-0995-4e51-9e41-82c208015c7f/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Peaceful Spirit Massage:
|
||||
https://login.microsoftonline.com/13be285a-374d-4a7c-a7d8-4cb5a98b5c29/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Putt Land Surveying Inc:
|
||||
https://login.microsoftonline.com/25008634-91b4-40aa-8113-78ea03826156/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Rednour Law:
|
||||
https://login.microsoftonline.com/4a4ca18a-f516-478b-99da-2e0722c5dc18/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Reliant Well Drilling and Pump:
|
||||
https://login.microsoftonline.com/2b124552-3891-4090-b3ed-2eebad3c4083/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Ridgetop Group:
|
||||
https://login.microsoftonline.com/ef111bfc-9c90-43c9-a581-f9bbfceb6517/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Rincon Vista Veterinary Center:
|
||||
https://login.microsoftonline.com/b8cdcd89-d0f4-4747-bcf3-8bd8a25fd7e1/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Russo Law Firm:
|
||||
https://login.microsoftonline.com/bef1b190-f78f-4b1c-aa4b-fab186a30702/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Safe Site Utility Services LLC:
|
||||
https://login.microsoftonline.com/71b4e637-c802-4137-a812-ae50dbc839e3/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
SANDTEKO MACHINERY LLC:
|
||||
https://login.microsoftonline.com/739bb777-cf76-478f-866b-f61c830c8246/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Shave, Kevin:
|
||||
https://login.microsoftonline.com/984c05a9-708b-4ec1-9f43-558865cb3c9d/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Sonorangreenllc.com:
|
||||
https://login.microsoftonline.com/ededa4fb-f6eb-4398-851d-5eb3e11fab27/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Starr Pass Realty:
|
||||
https://login.microsoftonline.com/222450dd-141f-435f-87b8-cec719aac99e/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
The Dumpster Guys:
|
||||
https://login.microsoftonline.com/0b3cd451-2679-4697-b161-07b9ef8d41e9/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
The Prairie Schooner:
|
||||
https://login.microsoftonline.com/c941033c-2752-42ef-be22-fbab77e2e587/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Tucson Golden Corral:
|
||||
https://login.microsoftonline.com/50e23e94-960f-4f61-8a27-97dbbe001a36/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Tucson Mountain Motors:
|
||||
https://login.microsoftonline.com/ffdabd05-236b-4666-a7f5-cc40ae9f9122/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Valley Wide Plastering:
|
||||
https://login.microsoftonline.com/5c53ae9f-7071-4248-b834-8685b646450f/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
|
||||
Von's Carstar:
|
||||
https://login.microsoftonline.com/53de51b9-a063-4f46-88ff-7c3468828ed9/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
188
.claude/skills/remediation-tool/scripts/get-token.sh
Executable file
188
.claude/skills/remediation-tool/scripts/get-token.sh
Executable file
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env bash
|
||||
# Acquire a client-credentials bearer token for a ComputerGuru MSP app tier.
|
||||
# Usage: get-token.sh <tenant-id-or-domain> <tier>
|
||||
#
|
||||
# Tiers and their app + resource scope:
|
||||
# investigator ComputerGuru Security Investigator -> Graph API (read-only breach checks)
|
||||
# investigator-exo ComputerGuru Security Investigator -> Exchange Online (EXO read: Get-InboxRule, Get-Mailbox)
|
||||
# exchange-op ComputerGuru Exchange Operator -> Exchange Online (EXO write: Set-Mailbox, Remove-InboxRule, revoke sessions)
|
||||
# user-manager ComputerGuru User Manager -> Graph API (user create/update/disable, license assign, MFA reset)
|
||||
# tenant-admin ComputerGuru Tenant Admin -> Graph API (app roles, CA policy, directory write — high privilege)
|
||||
# defender ComputerGuru Defender Add-on -> Defender ATP API (MDE-licensed tenants only)
|
||||
#
|
||||
# Output (stdout): bearer token. Exits 0 on success.
|
||||
# Cache: /tmp/remediation-tool/{tenant-id}/{tier}.jwt TTL 55 minutes.
|
||||
set -euo pipefail
|
||||
|
||||
TARGET="${1:?usage: get-token.sh <tenant-id|domain> <tier>}"
|
||||
TIER="${2:?usage: get-token.sh <tenant-id|domain> <tier>}"
|
||||
|
||||
# Resolve domain to tenant GUID if needed
|
||||
if [[ "$TARGET" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]]; then
|
||||
TENANT_ID="$TARGET"
|
||||
else
|
||||
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
||||
TENANT_ID=$("$SCRIPT_DIR/resolve-tenant.sh" "$TARGET")
|
||||
fi
|
||||
|
||||
# Map tier -> client_id, vault SOPS path, resource scope
|
||||
case "$TIER" in
|
||||
investigator)
|
||||
CLIENT_ID="bfbc12a4-f0dd-4e12-b06d-997e7271e10c"
|
||||
VAULT_PATH="msp-tools/computerguru-security-investigator.sops.yaml"
|
||||
SCOPE_URL="https://graph.microsoft.com/.default"
|
||||
;;
|
||||
investigator-exo)
|
||||
CLIENT_ID="bfbc12a4-f0dd-4e12-b06d-997e7271e10c"
|
||||
VAULT_PATH="msp-tools/computerguru-security-investigator.sops.yaml"
|
||||
SCOPE_URL="https://outlook.office365.com/.default"
|
||||
;;
|
||||
exchange-op)
|
||||
CLIENT_ID="b43e7342-5b4b-492f-890f-bb5a4f7f40e9"
|
||||
VAULT_PATH="msp-tools/computerguru-exchange-operator.sops.yaml"
|
||||
SCOPE_URL="https://outlook.office365.com/.default"
|
||||
;;
|
||||
user-manager)
|
||||
CLIENT_ID="64fac46b-8b44-41ad-93ee-7da03927576c"
|
||||
VAULT_PATH="msp-tools/computerguru-user-manager.sops.yaml"
|
||||
SCOPE_URL="https://graph.microsoft.com/.default"
|
||||
;;
|
||||
tenant-admin)
|
||||
CLIENT_ID="709e6eed-0711-4875-9c44-2d3518c47063"
|
||||
VAULT_PATH="msp-tools/computerguru-tenant-admin.sops.yaml"
|
||||
SCOPE_URL="https://graph.microsoft.com/.default"
|
||||
;;
|
||||
tenant-admin-onboard)
|
||||
# Same app as tenant-admin; this alias signals the token is being used
|
||||
# during initial tenant onboarding (clearer error messages on failure).
|
||||
CLIENT_ID="709e6eed-0711-4875-9c44-2d3518c47063"
|
||||
VAULT_PATH="msp-tools/computerguru-tenant-admin.sops.yaml"
|
||||
SCOPE_URL="https://graph.microsoft.com/.default"
|
||||
;;
|
||||
defender)
|
||||
CLIENT_ID="dbf8ad1a-54f4-4bb8-8a9e-ea5b9634635b"
|
||||
VAULT_PATH="msp-tools/computerguru-defender-addon.sops.yaml"
|
||||
SCOPE_URL="https://api.securitycenter.microsoft.com/.default"
|
||||
;;
|
||||
intune-manager)
|
||||
CLIENT_ID="46986910-aa47-4e5e-b596-f65c6b485abb"
|
||||
VAULT_PATH="msp-tools/computerguru-intune-manager.sops.yaml"
|
||||
SCOPE_URL="https://graph.microsoft.com/.default"
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: unknown tier '$TIER'." >&2
|
||||
echo "Valid tiers: investigator | investigator-exo | exchange-op | user-manager | tenant-admin | tenant-admin-onboard | defender | intune-manager" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
CACHE_DIR="/tmp/remediation-tool/$TENANT_ID"
|
||||
mkdir -p "$CACHE_DIR"
|
||||
CACHE_FILE="$CACHE_DIR/${TIER}.jwt"
|
||||
|
||||
# Return cached token if < 55 minutes old
|
||||
if [[ -f "$CACHE_FILE" ]] && [[ $(find "$CACHE_FILE" -mmin -55 2>/dev/null) ]]; then
|
||||
cat "$CACHE_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Locate vault repo via .claude/identity.json (per-machine, gitignored).
|
||||
# Falls back to VAULT_PATH env var if set.
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CLAUDETOOLS_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
IDENTITY_FILE="$CLAUDETOOLS_ROOT/.claude/identity.json"
|
||||
|
||||
VAULT_ROOT="${VAULT_ROOT_ENV:-}"
|
||||
if [[ -z "$VAULT_ROOT" && -f "$IDENTITY_FILE" ]]; then
|
||||
# Try jq first (handles Unix paths on Windows cleanly)
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
VAULT_ROOT=$(jq -r '.vault_path // empty' "$IDENTITY_FILE" 2>/dev/null)
|
||||
fi
|
||||
# Fall back to Python with Windows path conversion
|
||||
if [[ -z "$VAULT_ROOT" ]]; then
|
||||
IDENTITY_FILE_WIN=$(cygpath -w "$IDENTITY_FILE" 2>/dev/null || echo "$IDENTITY_FILE")
|
||||
for py in py python3 python; do
|
||||
if command -v "$py" >/dev/null 2>&1; then
|
||||
VAULT_ROOT=$("$py" -c "import json; print(json.load(open(r'${IDENTITY_FILE_WIN}')).get('vault_path',''))" 2>/dev/null) && break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
[[ -z "$VAULT_ROOT" ]] && { echo "ERROR: vault_path not set in $IDENTITY_FILE and VAULT_ROOT_ENV env var not set" >&2; exit 3; }
|
||||
[[ ! -d "$VAULT_ROOT" ]] && { echo "ERROR: vault not found at $VAULT_ROOT (check vault_path in $IDENTITY_FILE)" >&2; exit 3; }
|
||||
|
||||
SOPS_FILE="$VAULT_ROOT/$VAULT_PATH"
|
||||
[[ ! -f "$SOPS_FILE" ]] && { echo "ERROR: vault file not found: $SOPS_FILE" >&2; exit 3; }
|
||||
|
||||
# Read client secret via vault.sh (fast path), falling back to raw sops+python
|
||||
CLIENT_SECRET=""
|
||||
if [[ -f "$VAULT_ROOT/scripts/vault.sh" ]]; then
|
||||
# Try both field names — new files use client_secret, legacy used credential
|
||||
CLIENT_SECRET=$(bash "$VAULT_ROOT/scripts/vault.sh" get-field "$VAULT_PATH" credentials.client_secret 2>/dev/null | tr -d '\r\n' || true)
|
||||
if [[ -z "$CLIENT_SECRET" ]]; then
|
||||
CLIENT_SECRET=$(bash "$VAULT_ROOT/scripts/vault.sh" get-field "$VAULT_PATH" credentials.credential 2>/dev/null | tr -d '\r\n' || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$CLIENT_SECRET" ]]; then
|
||||
PYTHON_BIN=""
|
||||
for p in py python python3; do command -v "$p" >/dev/null 2>&1 && PYTHON_BIN="$p" && break; done
|
||||
[[ -z "$PYTHON_BIN" ]] && { echo "ERROR: vault.sh failed and python unavailable for SOPS fallback" >&2; exit 3; }
|
||||
command -v sops >/dev/null 2>&1 || { echo "ERROR: sops not on PATH (needed for fallback decrypt)" >&2; exit 3; }
|
||||
|
||||
CLIENT_SECRET=$(sops -d "$SOPS_FILE" 2>/dev/null | "$PYTHON_BIN" -c "
|
||||
import sys, re
|
||||
t = sys.stdin.read()
|
||||
m = re.search(r'^credentials:\s*\n((?:[ \t]+.*\n)+)', t, re.MULTILINE)
|
||||
if not m: sys.exit(1)
|
||||
for line in m.group(1).splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith('client_secret:') or line.startswith('credential:'):
|
||||
print(line.split(':', 1)[1].strip().strip('\"').strip(\"'\"))
|
||||
break
|
||||
" | tr -d '\r\n')
|
||||
fi
|
||||
|
||||
[[ -z "$CLIENT_SECRET" ]] && {
|
||||
echo "ERROR: could not read secret from $VAULT_PATH" >&2
|
||||
echo " Check field: credentials.client_secret (or credentials.credential for older entries)" >&2
|
||||
exit 4
|
||||
}
|
||||
|
||||
# Request token
|
||||
RESP=$(curl -s --max-time 15 -X POST \
|
||||
"https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token" \
|
||||
--data-urlencode "client_id=${CLIENT_ID}" \
|
||||
--data-urlencode "client_secret=${CLIENT_SECRET}" \
|
||||
--data-urlencode "scope=${SCOPE_URL}" \
|
||||
--data-urlencode "grant_type=client_credentials")
|
||||
|
||||
TOKEN=$(echo "$RESP" | jq -r '.access_token // empty')
|
||||
|
||||
if [[ -z "$TOKEN" ]]; then
|
||||
ERROR_CODE=$(echo "$RESP" | jq -r '.error_codes[0] // empty' 2>/dev/null || true)
|
||||
ERROR_DESC=$(echo "$RESP" | jq -r '.error_description // empty' 2>/dev/null || true)
|
||||
|
||||
# AADSTS7000229 — service principal not found in tenant (not consented)
|
||||
if echo "$ERROR_DESC" | grep -qi "7000229\|AADSTS7000229" || [[ "$ERROR_CODE" == "7000229" ]]; then
|
||||
echo "ERROR: AADSTS7000229 — app not consented in tenant $TENANT_ID (tier=$TIER)" >&2
|
||||
echo "" >&2
|
||||
echo " The '${TIER}' service principal has not been authorized in this tenant." >&2
|
||||
echo " Send this consent URL to the customer Global Admin:" >&2
|
||||
echo "" >&2
|
||||
echo " https://login.microsoftonline.com/${TENANT_ID}/adminconsent?client_id=${CLIENT_ID}&redirect_uri=https://azcomputerguru.com&prompt=consent" >&2
|
||||
echo "" >&2
|
||||
echo " After the admin accepts, run onboard-tenant.sh to assign required directory roles:" >&2
|
||||
SCRIPT_DIR_ERR="$(dirname "${BASH_SOURCE[0]}")"
|
||||
echo " bash ${SCRIPT_DIR_ERR}/onboard-tenant.sh ${TARGET}" >&2
|
||||
exit 5
|
||||
fi
|
||||
|
||||
echo "ERROR: token request failed (tenant=$TENANT_ID tier=$TIER)" >&2
|
||||
echo "$RESP" >&2
|
||||
exit 5
|
||||
fi
|
||||
|
||||
echo "$TOKEN" > "$CACHE_FILE"
|
||||
chmod 600 "$CACHE_FILE" 2>/dev/null || true
|
||||
echo "$TOKEN"
|
||||
566
.claude/skills/remediation-tool/scripts/onboard-tenant.sh
Executable file
566
.claude/skills/remediation-tool/scripts/onboard-tenant.sh
Executable file
@@ -0,0 +1,566 @@
|
||||
#!/usr/bin/env bash
|
||||
# Assign required Entra directory roles to ComputerGuru MSP service principals
|
||||
# in a newly-consented customer tenant, and programmatically consent all other
|
||||
# ComputerGuru apps so only Tenant Admin requires a manual customer consent click.
|
||||
#
|
||||
# Usage: onboard-tenant.sh <domain-or-tenant-id> [--dry-run]
|
||||
#
|
||||
# What this script does:
|
||||
# 1. Resolves the tenant ID
|
||||
# 2. Acquires a Tenant Admin token (fails gracefully if not consented)
|
||||
# 3. Creates SPs for Security Investigator, Exchange Operator, User Manager,
|
||||
# and Defender Add-on (equivalent to admin consent for each)
|
||||
# 4. Grants all required Graph/EXO/Defender appRoleAssignments to each SP
|
||||
# 5. Assigns required directory roles to each SP
|
||||
# 6. Prints a final status table
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 all roles present or successfully assigned
|
||||
# 1 resolve failure
|
||||
# 2 Tenant Admin not consented (consent URL printed)
|
||||
# 3 vault error
|
||||
# 10 partial failure (some roles could not be assigned)
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
TARGET="${1:?Usage: onboard-tenant.sh <domain-or-tenant-id> [--dry-run]}"
|
||||
DRY_RUN=false
|
||||
[[ "${2:-}" == "--dry-run" ]] && DRY_RUN=true
|
||||
|
||||
# ── App IDs ───────────────────────────────────────────────────────────────────
|
||||
APP_SEC_INV="bfbc12a4-f0dd-4e12-b06d-997e7271e10c"
|
||||
APP_EXCH_OP="b43e7342-5b4b-492f-890f-bb5a4f7f40e9"
|
||||
APP_USER_MGR="64fac46b-8b44-41ad-93ee-7da03927576c"
|
||||
APP_TENANT_ADMIN="709e6eed-0711-4875-9c44-2d3518c47063"
|
||||
APP_DEFENDER="dbf8ad1a-54f4-4bb8-8a9e-ea5b9634635b"
|
||||
|
||||
# ── Resource app IDs (well-known Microsoft multi-tenant apps) ─────────────────
|
||||
GRAPH_APP_ID="00000003-0000-0000-c000-000000000000"
|
||||
EXO_APP_ID="00000002-0000-0ff1-ce00-000000000000"
|
||||
DEFENDER_APP_ID="fc780465-2017-40d4-a0c5-307022471b92"
|
||||
|
||||
# ── Directory role GUIDs ──────────────────────────────────────────────────────
|
||||
ROLE_EXCHANGE_ADMIN="29232cdf-9323-42fd-ade2-1d097af3e4de"
|
||||
ROLE_USER_ADMIN="fe930be7-5e62-47db-91af-98c3a49a38b1"
|
||||
ROLE_AUTH_ADMIN="c4e39bd9-1100-46d3-8c65-fb160da0071f"
|
||||
|
||||
# ── Graph appRole GUIDs per app (from requiredResourceAccess in home tenant) ──
|
||||
# Security Investigator — Graph
|
||||
SEC_INV_GRAPH_ROLES=(
|
||||
"df021288-bdef-4463-88db-98f22de89214"
|
||||
"b0afded3-3588-46d8-8b3d-9842eff778da"
|
||||
"7ab1d382-f21e-4acd-a863-ba3e13f7da61"
|
||||
"40f97065-369a-49f4-947c-6a255697ae91"
|
||||
"810c84a8-4a9e-49e6-bf7d-12d183f40d01"
|
||||
"9a5d68dd-52b0-4cc2-bd40-abcf44ac3a30"
|
||||
"38d9df27-64da-44fd-b7c5-a6fbac20248f"
|
||||
"dc5007c0-2d7d-4c42-879c-2dab87571379"
|
||||
"246dd0d5-5bd0-4def-940b-0421030a5b68"
|
||||
"498476ce-e0fe-48b0-b801-37ba7e2685c6"
|
||||
)
|
||||
# Security Investigator — Exchange Online
|
||||
SEC_INV_EXO_ROLES=(
|
||||
"dc890d15-9560-4a4c-9b7f-a736ec74ec40"
|
||||
)
|
||||
|
||||
# Exchange Operator — Graph
|
||||
EXCH_OP_GRAPH_ROLES=(
|
||||
"df021288-bdef-4463-88db-98f22de89214"
|
||||
"6931bccd-447a-43d1-b442-00a195474933"
|
||||
"e2a3a72e-5f79-4c64-b1b1-878b674786c9"
|
||||
"77f3a031-c388-4f99-b373-dc68676a979e"
|
||||
"498476ce-e0fe-48b0-b801-37ba7e2685c6"
|
||||
)
|
||||
# Exchange Operator — Exchange Online
|
||||
EXCH_OP_EXO_ROLES=(
|
||||
"dc890d15-9560-4a4c-9b7f-a736ec74ec40"
|
||||
"dc50a0fb-09a3-484d-be87-e023b12c6440"
|
||||
)
|
||||
|
||||
# User Manager — Graph only
|
||||
USER_MGR_GRAPH_ROLES=(
|
||||
"741f803b-c850-494e-b5df-cde7c675a1ca"
|
||||
"19dbc75e-c2e2-444c-a770-ec69d8559fc7"
|
||||
"62a82d76-70ea-41e2-9197-370581804d09"
|
||||
"50483e42-d915-4231-9639-7fdb7fd190e5"
|
||||
"77f3a031-c388-4f99-b373-dc68676a979e"
|
||||
"498476ce-e0fe-48b0-b801-37ba7e2685c6"
|
||||
)
|
||||
|
||||
# Defender Add-on — Graph
|
||||
DEFENDER_GRAPH_ROLES=(
|
||||
"bf394140-e372-4bf9-a898-299cfc7564e5"
|
||||
)
|
||||
# Defender Add-on — Defender ATP
|
||||
DEFENDER_ATP_ROLES=(
|
||||
"71fe6b80-7034-4028-9ed8-0f316df9c3ff"
|
||||
"ea8291d3-4b9a-44b5-bc3a-6cea3026dc79"
|
||||
"93489bf5-0fbc-4f2d-b901-33f2fe08ff05"
|
||||
"41269fc5-d04d-4bfd-bce7-43a51cea049a"
|
||||
"528ca142-c849-4a5b-935e-10b8b9c38a84"
|
||||
)
|
||||
|
||||
CONSENT_BASE="https://login.microsoftonline.com"
|
||||
CONSENT_REDIRECT="https://azcomputerguru.com"
|
||||
|
||||
# ── Helper: print consent URLs for all apps ───────────────────────────────────
|
||||
print_consent_urls() {
|
||||
local tenant_id="$1"
|
||||
echo ""
|
||||
echo "[INFO] Consent URLs for tenant $tenant_id (provide to customer Global Admin):"
|
||||
echo " [1] Tenant Admin (consent FIRST — needed for programmatic onboarding of all other apps):"
|
||||
echo " ${CONSENT_BASE}/${tenant_id}/adminconsent?client_id=${APP_TENANT_ADMIN}&redirect_uri=${CONSENT_REDIRECT}&prompt=consent"
|
||||
echo ""
|
||||
echo " After the admin accepts Tenant Admin consent, run:"
|
||||
echo " bash $0 $TARGET"
|
||||
echo ""
|
||||
echo " The script will then automatically consent all other apps in the suite."
|
||||
echo ""
|
||||
echo " (Optional — only needed if Tenant Admin consent failed for individual apps):"
|
||||
echo " [2] Security Investigator: ${CONSENT_BASE}/${tenant_id}/adminconsent?client_id=${APP_SEC_INV}&redirect_uri=${CONSENT_REDIRECT}&prompt=consent"
|
||||
echo " [3] Exchange Operator: ${CONSENT_BASE}/${tenant_id}/adminconsent?client_id=${APP_EXCH_OP}&redirect_uri=${CONSENT_REDIRECT}&prompt=consent"
|
||||
echo " [4] User Manager: ${CONSENT_BASE}/${tenant_id}/adminconsent?client_id=${APP_USER_MGR}&redirect_uri=${CONSENT_REDIRECT}&prompt=consent"
|
||||
echo " [5] Defender Add-on (MDE-licensed tenants only): ${CONSENT_BASE}/${tenant_id}/adminconsent?client_id=${APP_DEFENDER}&redirect_uri=${CONSENT_REDIRECT}&prompt=consent"
|
||||
}
|
||||
|
||||
# ── Helper: get SP OID in tenant ──────────────────────────────────────────────
|
||||
get_sp_oid() {
|
||||
local token="$1"
|
||||
local app_id="$2"
|
||||
local resp
|
||||
resp=$(curl -s --max-time 15 \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-G \
|
||||
--data-urlencode "\$filter=appId eq '${app_id}'" \
|
||||
--data-urlencode "\$select=id,displayName" \
|
||||
"https://graph.microsoft.com/v1.0/servicePrincipals")
|
||||
echo "$resp" | jq -r '.value[0].id // empty'
|
||||
}
|
||||
|
||||
# ── Helper: create SP for our app if not present ──────────────────────────────
|
||||
create_sp_if_missing() {
|
||||
local token="$1"
|
||||
local app_id="$2"
|
||||
local app_name="$3"
|
||||
|
||||
local oid
|
||||
oid=$(get_sp_oid "$token" "$app_id")
|
||||
|
||||
if [[ -n "$oid" ]]; then
|
||||
echo " [OK] $app_name SP already present: $oid" >&2
|
||||
echo "$oid"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo " [DRY-RUN] Would create SP for $app_name ($app_id)" >&2
|
||||
echo ""
|
||||
return 0
|
||||
fi
|
||||
|
||||
local resp
|
||||
resp=$(curl -s --max-time 15 \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
"https://graph.microsoft.com/v1.0/servicePrincipals" \
|
||||
-d "{\"appId\": \"$app_id\"}")
|
||||
|
||||
local new_oid
|
||||
new_oid=$(echo "$resp" | jq -r '.id // empty')
|
||||
|
||||
if [[ -z "$new_oid" ]]; then
|
||||
local err_code
|
||||
err_code=$(echo "$resp" | jq -r '.error.code // empty')
|
||||
if [[ "$err_code" == "Request_MultipleObjectsWithSameKeyValue" ]] || echo "$resp" | grep -qi "conflicting"; then
|
||||
oid=$(get_sp_oid "$token" "$app_id")
|
||||
echo " [OK] $app_name SP already exists: $oid" >&2
|
||||
echo "$oid"
|
||||
return 0
|
||||
fi
|
||||
echo " [ERROR] Failed to create SP for $app_name: $(echo "$resp" | jq -r '.error.message // empty')" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo " [CREATED] $app_name SP: $new_oid" >&2
|
||||
# Brief pause for Graph replication before granting appRoleAssignments
|
||||
sleep 5
|
||||
echo "$new_oid"
|
||||
}
|
||||
|
||||
# ── Helper: grant single appRoleAssignment (idempotent) ───────────────────────
|
||||
grant_app_role() {
|
||||
local token="$1"
|
||||
local principal_oid="$2"
|
||||
local resource_oid="$3"
|
||||
local role_id="$4"
|
||||
|
||||
# Check if already granted
|
||||
local already
|
||||
already=$(curl -s --max-time 15 \
|
||||
-H "Authorization: Bearer $token" \
|
||||
"https://graph.microsoft.com/v1.0/servicePrincipals/$principal_oid/appRoleAssignments" \
|
||||
| jq --arg rid "$role_id" '([.value[]? | select(.appRoleId == $rid)] | length) > 0')
|
||||
|
||||
if [[ "$already" == "true" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo " [DRY-RUN] Would grant role $role_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local body
|
||||
body=$(jq -n \
|
||||
--arg principal "$principal_oid" \
|
||||
--arg resource "$resource_oid" \
|
||||
--arg role "$role_id" \
|
||||
'{"principalId": $principal, "resourceId": $resource, "appRoleId": $role}')
|
||||
|
||||
local resp
|
||||
resp=$(curl -s --max-time 15 \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
"https://graph.microsoft.com/v1.0/servicePrincipals/$principal_oid/appRoleAssignments" \
|
||||
-d "$body")
|
||||
|
||||
local granted_id
|
||||
granted_id=$(echo "$resp" | jq -r '.id // empty')
|
||||
|
||||
if [[ -z "$granted_id" ]]; then
|
||||
local err_code
|
||||
err_code=$(echo "$resp" | jq -r '.error.code // empty')
|
||||
if [[ "$err_code" == "Request_MultipleObjectsWithSameKeyValue" ]] || echo "$resp" | grep -qi "conflicting"; then
|
||||
return 0
|
||||
fi
|
||||
echo " [ERROR] grant_app_role failed for $role_id: $(echo "$resp" | jq -r '.error.message // "unknown"')" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Helper: consent app + grant all permissions ───────────────────────────────
|
||||
# Usage: consent_app <token> <app_id> <app_name> \
|
||||
# <graph_sp_oid> <exo_sp_oid_or_empty> <defender_sp_oid_or_empty> \
|
||||
# <graph_roles_varname> [<exo_roles_varname>] [<atp_roles_varname>]
|
||||
consent_app() {
|
||||
local token="$1"
|
||||
local app_id="$2"
|
||||
local app_name="$3"
|
||||
local graph_sp_oid="$4"
|
||||
local exo_sp_oid="${5:-}"
|
||||
local defender_sp_oid="${6:-}"
|
||||
local graph_roles_varname="$7"
|
||||
local exo_roles_varname="${8:-}"
|
||||
local atp_roles_varname="${9:-}"
|
||||
|
||||
echo ""
|
||||
echo "[CONSENT] $app_name ($app_id)"
|
||||
|
||||
# Create SP (or confirm existing)
|
||||
local sp_oid
|
||||
sp_oid=$(create_sp_if_missing "$token" "$app_id" "$app_name")
|
||||
if [[ -z "$sp_oid" ]]; then
|
||||
echo " [ERROR] Cannot proceed — SP creation failed" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local grant_errors=0
|
||||
|
||||
# Grant Graph permissions
|
||||
eval "local graph_roles=(\"\${${graph_roles_varname}[@]}\")"
|
||||
local granted=0 skipped=0 errors=0
|
||||
for role_id in "${graph_roles[@]}"; do
|
||||
if grant_app_role "$token" "$sp_oid" "$graph_sp_oid" "$role_id"; then
|
||||
((granted++)) || true
|
||||
else
|
||||
((errors++)) || true
|
||||
((grant_errors++)) || true
|
||||
fi
|
||||
done
|
||||
echo " Graph permissions: ${#graph_roles[@]} total — $errors error(s)"
|
||||
|
||||
# Grant Exchange Online permissions
|
||||
if [[ -n "$exo_roles_varname" ]] && [[ -n "$exo_sp_oid" ]]; then
|
||||
eval "local exo_roles=(\"\${${exo_roles_varname}[@]}\")"
|
||||
local exo_errors=0
|
||||
for role_id in "${exo_roles[@]}"; do
|
||||
if ! grant_app_role "$token" "$sp_oid" "$exo_sp_oid" "$role_id"; then
|
||||
((exo_errors++)) || true
|
||||
((grant_errors++)) || true
|
||||
fi
|
||||
done
|
||||
echo " Exchange Online permissions: ${#exo_roles[@]} total — $exo_errors error(s)"
|
||||
elif [[ -n "$exo_roles_varname" ]] && [[ -z "$exo_sp_oid" ]]; then
|
||||
echo " [WARNING] Exchange Online SP not found — EXO permissions skipped"
|
||||
fi
|
||||
|
||||
# Grant Defender ATP permissions
|
||||
if [[ -n "$atp_roles_varname" ]] && [[ -n "$defender_sp_oid" ]]; then
|
||||
eval "local atp_roles=(\"\${${atp_roles_varname}[@]}\")"
|
||||
local atp_errors=0
|
||||
for role_id in "${atp_roles[@]}"; do
|
||||
if ! grant_app_role "$token" "$sp_oid" "$defender_sp_oid" "$role_id"; then
|
||||
((atp_errors++)) || true
|
||||
((grant_errors++)) || true
|
||||
fi
|
||||
done
|
||||
echo " Defender ATP permissions: ${#atp_roles[@]} total — $atp_errors error(s)"
|
||||
elif [[ -n "$atp_roles_varname" ]] && [[ -z "$defender_sp_oid" ]]; then
|
||||
echo " [INFO] Defender ATP SP not found — tenant likely not MDE-licensed, skipping"
|
||||
fi
|
||||
|
||||
if [[ $grant_errors -eq 0 ]]; then
|
||||
echo " [OK] $app_name fully consented and permissions granted"
|
||||
return 0
|
||||
else
|
||||
echo " [WARNING] $app_name consent completed with $grant_errors permission error(s)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Helper: check if directory role already assigned ─────────────────────────
|
||||
role_assigned() {
|
||||
local token="$1"
|
||||
local sp_oid="$2"
|
||||
local role_id="$3"
|
||||
local resp
|
||||
resp=$(curl -s --max-time 15 \
|
||||
-H "Authorization: Bearer $token" \
|
||||
"https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?\$filter=principalId eq '${sp_oid}'")
|
||||
echo "$resp" | jq --arg rid "$role_id" \
|
||||
'[.value[] | select(.roleDefinitionId == $rid)] | length > 0'
|
||||
}
|
||||
|
||||
# ── Helper: assign directory role ─────────────────────────────────────────────
|
||||
assign_role() {
|
||||
local token="$1"
|
||||
local sp_oid="$2"
|
||||
local role_id="$3"
|
||||
local role_name="$4"
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo " [DRY-RUN] Would assign $role_name to SP $sp_oid"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local body
|
||||
body=$(jq -n \
|
||||
--arg role "$role_id" \
|
||||
--arg principal "$sp_oid" \
|
||||
'{"roleDefinitionId": $role, "principalId": $principal, "directoryScopeId": "/"}')
|
||||
|
||||
local resp
|
||||
resp=$(curl -s --max-time 15 \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
"https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments" \
|
||||
-d "$body")
|
||||
|
||||
local assigned_id
|
||||
assigned_id=$(echo "$resp" | jq -r '.id // empty')
|
||||
if [[ -z "$assigned_id" ]]; then
|
||||
local err_code
|
||||
err_code=$(echo "$resp" | jq -r '.error.code // empty')
|
||||
if [[ "$err_code" == "Conflict" ]] || [[ "$err_code" == "Request_MultipleObjectsWithSameKeyValue" ]] || \
|
||||
echo "$resp" | grep -qi "conflicting object"; then
|
||||
echo " [OK] $role_name already assigned (conflict returned — idempotent)"
|
||||
return 0
|
||||
fi
|
||||
echo " [ERROR] Failed to assign $role_name" >&2
|
||||
echo " Response: $resp" >&2
|
||||
return 1
|
||||
fi
|
||||
echo " [OK] $role_name assigned (assignment id=$assigned_id)"
|
||||
}
|
||||
|
||||
# ── Step 1: Resolve tenant ────────────────────────────────────────────────────
|
||||
echo "[INFO] Resolving tenant: $TARGET"
|
||||
TENANT_ID=$("$SCRIPT_DIR/resolve-tenant.sh" "$TARGET")
|
||||
if [[ -z "$TENANT_ID" ]]; then
|
||||
echo "[ERROR] Could not resolve tenant ID for: $TARGET" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DISPLAY_NAME="$TARGET"
|
||||
echo "[OK] Tenant: $DISPLAY_NAME ($TENANT_ID)"
|
||||
|
||||
# ── Step 2: Acquire Tenant Admin token ───────────────────────────────────────
|
||||
echo "[INFO] Acquiring Tenant Admin token for $TENANT_ID..."
|
||||
set +e
|
||||
TENANT_ADMIN_TOKEN_OUT=$("$SCRIPT_DIR/get-token.sh" "$TENANT_ID" "tenant-admin" 2>/tmp/onboard-token-err.txt)
|
||||
GET_TOKEN_EXIT=$?
|
||||
TOKEN_ERR=$(cat /tmp/onboard-token-err.txt 2>/dev/null || true)
|
||||
set -e
|
||||
|
||||
if [[ $GET_TOKEN_EXIT -ne 0 ]]; then
|
||||
if echo "$TOKEN_ERR" | grep -qi "7000229\|AADSTS7000229\|service principal\|not been authorized\|not found"; then
|
||||
echo "[WARNING] Tenant Admin app not yet consented in tenant $TENANT_ID"
|
||||
print_consent_urls "$TENANT_ID"
|
||||
exit 2
|
||||
fi
|
||||
echo "[ERROR] Failed to acquire Tenant Admin token (exit $GET_TOKEN_EXIT)" >&2
|
||||
echo "$TOKEN_ERR" >&2
|
||||
exit 5
|
||||
fi
|
||||
TENANT_ADMIN_TOKEN="$TENANT_ADMIN_TOKEN_OUT"
|
||||
|
||||
TA_SP_OID=$(get_sp_oid "$TENANT_ADMIN_TOKEN" "$APP_TENANT_ADMIN")
|
||||
if [[ -z "$TA_SP_OID" ]]; then
|
||||
echo "[WARNING] Tenant Admin SP not found in tenant — app not consented yet"
|
||||
print_consent_urls "$TENANT_ID"
|
||||
exit 2
|
||||
fi
|
||||
echo "[OK] Tenant Admin consented — SP: $TA_SP_OID"
|
||||
|
||||
[[ "$DRY_RUN" == "true" ]] && echo "[INFO] --dry-run mode: no changes will be made"
|
||||
|
||||
# ── Step 3: Locate resource SPs in customer tenant ───────────────────────────
|
||||
echo ""
|
||||
echo "[INFO] Locating resource service principals in tenant..."
|
||||
|
||||
GRAPH_SP_OID=$(get_sp_oid "$TENANT_ADMIN_TOKEN" "$GRAPH_APP_ID")
|
||||
EXO_SP_OID=$(get_sp_oid "$TENANT_ADMIN_TOKEN" "$EXO_APP_ID")
|
||||
DEFENDER_SP_OID=$(get_sp_oid "$TENANT_ADMIN_TOKEN" "$DEFENDER_APP_ID")
|
||||
|
||||
[[ -n "$GRAPH_SP_OID" ]] && echo " [OK] Microsoft Graph SP: $GRAPH_SP_OID" || echo " [ERROR] Microsoft Graph SP not found — cannot proceed" >&2
|
||||
[[ -n "$EXO_SP_OID" ]] && echo " [OK] Exchange Online SP: $EXO_SP_OID" || echo " [WARNING] Exchange Online SP not found (no Exchange license?)"
|
||||
[[ -n "$DEFENDER_SP_OID" ]] && echo " [OK] Defender ATP SP: $DEFENDER_SP_OID" || echo " [INFO] Defender ATP SP not found (tenant likely not MDE-licensed)"
|
||||
|
||||
if [[ -z "$GRAPH_SP_OID" ]]; then
|
||||
echo "[ERROR] Microsoft Graph SP missing — cannot grant app permissions" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Step 4: Programmatic consent — create SPs + grant appRoleAssignments ──────
|
||||
echo ""
|
||||
echo "[INFO] Consenting app suite in tenant (programmatic — no customer click needed)..."
|
||||
|
||||
CONSENT_PARTIAL=false
|
||||
|
||||
consent_app "$TENANT_ADMIN_TOKEN" "$APP_SEC_INV" "Security Investigator" \
|
||||
"$GRAPH_SP_OID" "$EXO_SP_OID" "" \
|
||||
"SEC_INV_GRAPH_ROLES" "SEC_INV_EXO_ROLES" \
|
||||
|| CONSENT_PARTIAL=true
|
||||
|
||||
consent_app "$TENANT_ADMIN_TOKEN" "$APP_EXCH_OP" "Exchange Operator" \
|
||||
"$GRAPH_SP_OID" "$EXO_SP_OID" "" \
|
||||
"EXCH_OP_GRAPH_ROLES" "EXCH_OP_EXO_ROLES" \
|
||||
|| CONSENT_PARTIAL=true
|
||||
|
||||
consent_app "$TENANT_ADMIN_TOKEN" "$APP_USER_MGR" "User Manager" \
|
||||
"$GRAPH_SP_OID" "" "" \
|
||||
"USER_MGR_GRAPH_ROLES" \
|
||||
|| CONSENT_PARTIAL=true
|
||||
|
||||
if [[ -n "$DEFENDER_SP_OID" ]]; then
|
||||
consent_app "$TENANT_ADMIN_TOKEN" "$APP_DEFENDER" "Defender Add-on" \
|
||||
"$GRAPH_SP_OID" "" "$DEFENDER_SP_OID" \
|
||||
"DEFENDER_GRAPH_ROLES" "" "DEFENDER_ATP_ROLES" \
|
||||
|| CONSENT_PARTIAL=true
|
||||
else
|
||||
echo ""
|
||||
echo "[INFO] Skipping Defender Add-on consent (no MDE license detected)"
|
||||
fi
|
||||
|
||||
# ── Step 5: Check/assign directory roles per SP ───────────────────────────────
|
||||
declare -A STATUS_MAP
|
||||
|
||||
echo ""
|
||||
echo "[INFO] Checking and assigning directory roles..."
|
||||
|
||||
SEC_INV_OID=$(get_sp_oid "$TENANT_ADMIN_TOKEN" "$APP_SEC_INV")
|
||||
USER_MGR_OID=$(get_sp_oid "$TENANT_ADMIN_TOKEN" "$APP_USER_MGR")
|
||||
|
||||
PARTIAL_FAILURE=false
|
||||
|
||||
# Security Investigator -> Exchange Administrator
|
||||
if [[ -z "$SEC_INV_OID" ]]; then
|
||||
echo "[WARNING] Security Investigator SP still not found after consent attempt"
|
||||
STATUS_MAP["Security Investigator:Exchange Administrator"]="MISSING SP"
|
||||
else
|
||||
echo ""
|
||||
echo "[CHECK] Security Investigator SP: $SEC_INV_OID"
|
||||
IS_PRESENT=$(role_assigned "$TENANT_ADMIN_TOKEN" "$SEC_INV_OID" "$ROLE_EXCHANGE_ADMIN")
|
||||
if [[ "$IS_PRESENT" == "true" ]]; then
|
||||
echo " Exchange Administrator: PRESENT"
|
||||
STATUS_MAP["Security Investigator:Exchange Administrator"]="OK"
|
||||
else
|
||||
echo " Exchange Administrator: MISSING -> ASSIGNING..."
|
||||
if assign_role "$TENANT_ADMIN_TOKEN" "$SEC_INV_OID" "$ROLE_EXCHANGE_ADMIN" "Exchange Administrator"; then
|
||||
STATUS_MAP["Security Investigator:Exchange Administrator"]=$( [[ "$DRY_RUN" == "true" ]] && echo "DRY-RUN" || echo "ASSIGNED" )
|
||||
else
|
||||
STATUS_MAP["Security Investigator:Exchange Administrator"]="ERROR"
|
||||
PARTIAL_FAILURE=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# User Manager -> User Administrator + Authentication Administrator
|
||||
if [[ -z "$USER_MGR_OID" ]]; then
|
||||
echo "[WARNING] User Manager SP still not found after consent attempt"
|
||||
STATUS_MAP["User Manager:User Administrator"]="MISSING SP"
|
||||
STATUS_MAP["User Manager:Authentication Administrator"]="MISSING SP"
|
||||
else
|
||||
echo ""
|
||||
echo "[CHECK] User Manager SP: $USER_MGR_OID"
|
||||
|
||||
IS_UA=$(role_assigned "$TENANT_ADMIN_TOKEN" "$USER_MGR_OID" "$ROLE_USER_ADMIN")
|
||||
if [[ "$IS_UA" == "true" ]]; then
|
||||
echo " User Administrator: PRESENT"
|
||||
STATUS_MAP["User Manager:User Administrator"]="OK"
|
||||
else
|
||||
echo " User Administrator: MISSING -> ASSIGNING..."
|
||||
if assign_role "$TENANT_ADMIN_TOKEN" "$USER_MGR_OID" "$ROLE_USER_ADMIN" "User Administrator"; then
|
||||
STATUS_MAP["User Manager:User Administrator"]=$( [[ "$DRY_RUN" == "true" ]] && echo "DRY-RUN" || echo "ASSIGNED" )
|
||||
else
|
||||
STATUS_MAP["User Manager:User Administrator"]="ERROR"
|
||||
PARTIAL_FAILURE=true
|
||||
fi
|
||||
fi
|
||||
|
||||
IS_AA=$(role_assigned "$TENANT_ADMIN_TOKEN" "$USER_MGR_OID" "$ROLE_AUTH_ADMIN")
|
||||
if [[ "$IS_AA" == "true" ]]; then
|
||||
echo " Authentication Administrator: PRESENT"
|
||||
STATUS_MAP["User Manager:Authentication Administrator"]="OK"
|
||||
else
|
||||
echo " Authentication Administrator: MISSING -> ASSIGNING..."
|
||||
if assign_role "$TENANT_ADMIN_TOKEN" "$USER_MGR_OID" "$ROLE_AUTH_ADMIN" "Authentication Administrator"; then
|
||||
STATUS_MAP["User Manager:Authentication Administrator"]=$( [[ "$DRY_RUN" == "true" ]] && echo "DRY-RUN" || echo "ASSIGNED" )
|
||||
else
|
||||
STATUS_MAP["User Manager:Authentication Administrator"]="ERROR"
|
||||
PARTIAL_FAILURE=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Step 6: Final status table ────────────────────────────────────────────────
|
||||
echo ""
|
||||
if [[ "$PARTIAL_FAILURE" == "true" ]] || [[ "$CONSENT_PARTIAL" == "true" ]]; then
|
||||
echo "[WARNING] Onboarding completed with errors for $DISPLAY_NAME"
|
||||
else
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo "[INFO] Dry-run complete for $DISPLAY_NAME ($TENANT_ID) — no changes made"
|
||||
else
|
||||
echo "[SUCCESS] Onboarding complete for $DISPLAY_NAME"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "SP roles status:"
|
||||
SEC_EXCH="${STATUS_MAP["Security Investigator:Exchange Administrator"]:-SKIPPED}"
|
||||
echo " Security Investigator:"
|
||||
printf " Exchange Administrator: %s\n" "[$SEC_EXCH]"
|
||||
|
||||
UA="${STATUS_MAP["User Manager:User Administrator"]:-SKIPPED}"
|
||||
AA="${STATUS_MAP["User Manager:Authentication Administrator"]:-SKIPPED}"
|
||||
echo " User Manager:"
|
||||
printf " User Administrator: %s\n" "[$UA]"
|
||||
printf " Authentication Administrator: %s\n" "[$AA]"
|
||||
|
||||
if [[ "$PARTIAL_FAILURE" == "true" ]]; then
|
||||
exit 10
|
||||
fi
|
||||
exit 0
|
||||
181
.claude/skills/remediation-tool/scripts/patch-tenant-admin-manifest.sh
Executable file
181
.claude/skills/remediation-tool/scripts/patch-tenant-admin-manifest.sh
Executable file
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env bash
|
||||
# Patch the Tenant Admin app manifest to add RoleManagement.ReadWrite.Directory,
|
||||
# then grant admin consent for that role in the ACG home tenant.
|
||||
#
|
||||
# Usage: patch-tenant-admin-manifest.sh
|
||||
#
|
||||
# Requirements:
|
||||
# - Management app token (ACG home tenant) via SOPS vault
|
||||
# - jq on PATH
|
||||
# - curl on PATH
|
||||
set -euo pipefail
|
||||
|
||||
ACG_HOME_TENANT="ce61461e-81a0-4c84-bb4a-7b354a9a356d"
|
||||
MANAGEMENT_CLIENT_ID="0df4e185-4cf2-478c-a490-cc4ef36c6118"
|
||||
MANAGEMENT_VAULT_PATH="msp-tools/computerguru-management.sops.yaml"
|
||||
TENANT_ADMIN_APP_ID="709e6eed-0711-4875-9c44-2d3518c47063"
|
||||
GRAPH_RESOURCE_APP_ID="00000003-0000-0000-c000-000000000000"
|
||||
ROLE_MGMT_PERMISSION_ID="9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CLAUDETOOLS_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
IDENTITY_FILE="$CLAUDETOOLS_ROOT/.claude/identity.json"
|
||||
|
||||
VAULT_ROOT="${VAULT_PATH:-}"
|
||||
if [[ -z "$VAULT_ROOT" && -f "$IDENTITY_FILE" ]]; then
|
||||
for py in py python3 python; do
|
||||
if command -v "$py" >/dev/null 2>&1; then
|
||||
VAULT_ROOT=$("$py" -c "import json; print(json.load(open('$IDENTITY_FILE')).get('vault_path',''))" 2>/dev/null) && break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
[[ -z "$VAULT_ROOT" ]] && { echo "[ERROR] vault_path not set in $IDENTITY_FILE and VAULT_PATH env var not set" >&2; exit 3; }
|
||||
[[ ! -d "$VAULT_ROOT" ]] && { echo "[ERROR] vault not found at $VAULT_ROOT (check vault_path in $IDENTITY_FILE)" >&2; exit 3; }
|
||||
|
||||
# ── Step 1: Get Management app client secret ──────────────────────────────────
|
||||
echo "[INFO] Reading Management app secret from vault..."
|
||||
CLIENT_SECRET=""
|
||||
CLIENT_SECRET=$(bash "$VAULT_ROOT/scripts/vault.sh" get-field "$MANAGEMENT_VAULT_PATH" credentials.client_secret 2>/dev/null | tr -d '\r\n' || true)
|
||||
if [[ -z "$CLIENT_SECRET" ]]; then
|
||||
CLIENT_SECRET=$(bash "$VAULT_ROOT/scripts/vault.sh" get-field "$MANAGEMENT_VAULT_PATH" credentials.credential 2>/dev/null | tr -d '\r\n' || true)
|
||||
fi
|
||||
[[ -z "$CLIENT_SECRET" ]] && { echo "[ERROR] Could not read secret from $MANAGEMENT_VAULT_PATH" >&2; exit 4; }
|
||||
echo "[OK] Management app secret retrieved"
|
||||
|
||||
# ── Step 2: Get Management app token (home tenant) ───────────────────────────
|
||||
echo "[INFO] Acquiring Management app token for ACG home tenant..."
|
||||
TOKEN_RESP=$(curl -s --max-time 15 -X POST \
|
||||
"https://login.microsoftonline.com/${ACG_HOME_TENANT}/oauth2/v2.0/token" \
|
||||
--data-urlencode "client_id=${MANAGEMENT_CLIENT_ID}" \
|
||||
--data-urlencode "client_secret=${CLIENT_SECRET}" \
|
||||
--data-urlencode "scope=https://graph.microsoft.com/.default" \
|
||||
--data-urlencode "grant_type=client_credentials")
|
||||
|
||||
MGMT_TOKEN=$(echo "$TOKEN_RESP" | jq -r '.access_token // empty')
|
||||
if [[ -z "$MGMT_TOKEN" ]]; then
|
||||
echo "[ERROR] Failed to acquire Management app token" >&2
|
||||
echo "$TOKEN_RESP" >&2
|
||||
exit 5
|
||||
fi
|
||||
echo "[OK] Management app token acquired"
|
||||
|
||||
# ── Step 3: Get current Tenant Admin app object + requiredResourceAccess ──────
|
||||
echo "[INFO] Fetching Tenant Admin app registration (appId=$TENANT_ADMIN_APP_ID)..."
|
||||
APP_RESP=$(curl -s --max-time 15 \
|
||||
-H "Authorization: Bearer $MGMT_TOKEN" \
|
||||
-G \
|
||||
--data-urlencode "\$filter=appId eq '$TENANT_ADMIN_APP_ID'" \
|
||||
--data-urlencode "\$select=id,displayName,requiredResourceAccess" \
|
||||
"https://graph.microsoft.com/v1.0/applications")
|
||||
|
||||
APP_OBJ_ID=$(echo "$APP_RESP" | jq -r '.value[0].id // empty')
|
||||
APP_DISPLAY=$(echo "$APP_RESP" | jq -r '.value[0].displayName // empty')
|
||||
if [[ -z "$APP_OBJ_ID" ]]; then
|
||||
echo "[ERROR] Tenant Admin application not found (appId=$TENANT_ADMIN_APP_ID)" >&2
|
||||
echo "Response: $APP_RESP" >&2
|
||||
exit 6
|
||||
fi
|
||||
echo "[OK] Found app: $APP_DISPLAY (objectId=$APP_OBJ_ID)"
|
||||
|
||||
# ── Step 4: Check whether RoleManagement.ReadWrite.Directory already present ──
|
||||
ALREADY_PRESENT=$(echo "$APP_RESP" | jq --arg pid "$ROLE_MGMT_PERMISSION_ID" \
|
||||
'[.value[0].requiredResourceAccess[].resourceAccess[].id] | map(select(. == $pid)) | length > 0')
|
||||
if [[ "$ALREADY_PRESENT" == "true" ]]; then
|
||||
echo "[OK] RoleManagement.ReadWrite.Directory already present in manifest — no patch needed"
|
||||
else
|
||||
echo "[INFO] RoleManagement.ReadWrite.Directory not in manifest — patching..."
|
||||
|
||||
# Build updated requiredResourceAccess: inject new permission into the Graph entry
|
||||
UPDATED_RRA=$(echo "$APP_RESP" | jq --arg gid "$GRAPH_RESOURCE_APP_ID" \
|
||||
--arg pid "$ROLE_MGMT_PERMISSION_ID" '
|
||||
.value[0].requiredResourceAccess
|
||||
| map(
|
||||
if .resourceAppId == $gid
|
||||
then .resourceAccess += [{"id": $pid, "type": "Role"}]
|
||||
else .
|
||||
end
|
||||
)
|
||||
')
|
||||
|
||||
# PATCH the application
|
||||
PATCH_RESP=$(curl -s --max-time 15 -o /dev/null -w "%{http_code}" -X PATCH \
|
||||
-H "Authorization: Bearer $MGMT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://graph.microsoft.com/v1.0/applications/$APP_OBJ_ID" \
|
||||
-d "{\"requiredResourceAccess\": $UPDATED_RRA}")
|
||||
|
||||
if [[ "$PATCH_RESP" == "204" ]]; then
|
||||
echo "[OK] App manifest patched (HTTP 204)"
|
||||
else
|
||||
echo "[ERROR] PATCH returned HTTP $PATCH_RESP" >&2
|
||||
exit 7
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Step 5: Locate Tenant Admin SP and Graph SP in the home tenant ─────────────
|
||||
echo "[INFO] Locating Tenant Admin service principal in home tenant..."
|
||||
TA_SP_RESP=$(curl -s --max-time 15 \
|
||||
-H "Authorization: Bearer $MGMT_TOKEN" \
|
||||
-G \
|
||||
--data-urlencode "\$filter=appId eq '$TENANT_ADMIN_APP_ID'" \
|
||||
--data-urlencode "\$select=id,displayName" \
|
||||
"https://graph.microsoft.com/v1.0/servicePrincipals")
|
||||
TA_SP_OID=$(echo "$TA_SP_RESP" | jq -r '.value[0].id // empty')
|
||||
[[ -z "$TA_SP_OID" ]] && { echo "[ERROR] Tenant Admin SP not found in home tenant" >&2; exit 8; }
|
||||
echo "[OK] Tenant Admin SP: $TA_SP_OID"
|
||||
|
||||
echo "[INFO] Locating Microsoft Graph SP in home tenant..."
|
||||
GRAPH_SP_RESP=$(curl -s --max-time 15 \
|
||||
-H "Authorization: Bearer $MGMT_TOKEN" \
|
||||
-G \
|
||||
--data-urlencode "\$filter=appId eq '00000003-0000-0000-c000-000000000000'" \
|
||||
--data-urlencode "\$select=id" \
|
||||
"https://graph.microsoft.com/v1.0/servicePrincipals")
|
||||
GRAPH_SP_OID=$(echo "$GRAPH_SP_RESP" | jq -r '.value[0].id // empty')
|
||||
[[ -z "$GRAPH_SP_OID" ]] && { echo "[ERROR] Microsoft Graph SP not found in home tenant" >&2; exit 8; }
|
||||
echo "[OK] Microsoft Graph SP: $GRAPH_SP_OID"
|
||||
|
||||
# ── Step 6: Check if appRoleAssignment already granted ────────────────────────
|
||||
echo "[INFO] Checking existing appRoleAssignments for Tenant Admin SP..."
|
||||
EXISTING_RESP=$(curl -s --max-time 15 \
|
||||
-H "Authorization: Bearer $MGMT_TOKEN" \
|
||||
"https://graph.microsoft.com/v1.0/servicePrincipals/$TA_SP_OID/appRoleAssignments")
|
||||
|
||||
ALREADY_GRANTED=$(echo "$EXISTING_RESP" | jq --arg rid "$ROLE_MGMT_PERMISSION_ID" \
|
||||
'[.value[] | select(.appRoleId == $rid)] | length > 0')
|
||||
|
||||
if [[ "$ALREADY_GRANTED" == "true" ]]; then
|
||||
echo "[OK] RoleManagement.ReadWrite.Directory appRoleAssignment already granted in home tenant — nothing to do"
|
||||
else
|
||||
echo "[INFO] Granting RoleManagement.ReadWrite.Directory appRoleAssignment in home tenant..."
|
||||
GRANT_BODY=$(jq -n \
|
||||
--arg principal "$TA_SP_OID" \
|
||||
--arg resource "$GRAPH_SP_OID" \
|
||||
--arg role "$ROLE_MGMT_PERMISSION_ID" \
|
||||
'{"principalId": $principal, "resourceId": $resource, "appRoleId": $role}')
|
||||
|
||||
GRANT_RESP=$(curl -s --max-time 15 \
|
||||
-H "Authorization: Bearer $MGMT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
"https://graph.microsoft.com/v1.0/servicePrincipals/$TA_SP_OID/appRoleAssignments" \
|
||||
-d "$GRANT_BODY")
|
||||
|
||||
GRANT_ID=$(echo "$GRANT_RESP" | jq -r '.id // empty')
|
||||
if [[ -z "$GRANT_ID" ]]; then
|
||||
echo "[ERROR] Failed to grant appRoleAssignment" >&2
|
||||
echo "$GRANT_RESP" >&2
|
||||
exit 9
|
||||
fi
|
||||
echo "[OK] appRoleAssignment granted (id=$GRANT_ID)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "[SUCCESS] Tenant Admin app manifest patched and home-tenant consent granted."
|
||||
echo " RoleManagement.ReadWrite.Directory (id=$ROLE_MGMT_PERMISSION_ID) is now active."
|
||||
echo ""
|
||||
echo "[INFO] Next step: re-run admin consent in any customer tenants where Tenant Admin"
|
||||
echo " is already consented, so the new permission is reflected in their service principal."
|
||||
echo ""
|
||||
echo " Consent URL pattern:"
|
||||
echo " https://login.microsoftonline.com/{TENANT_ID}/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent"
|
||||
37
.claude/skills/remediation-tool/scripts/resolve-tenant.sh
Executable file
37
.claude/skills/remediation-tool/scripts/resolve-tenant.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
# Resolve an M365 domain (or UPN) to a tenant GUID via OpenID discovery.
|
||||
# Usage: resolve-tenant.sh <domain-or-upn-or-tenantid>
|
||||
# Output (stdout): tenant GUID. Exit 0 on success, 1 on failure.
|
||||
set -euo pipefail
|
||||
|
||||
INPUT="${1:?usage: resolve-tenant.sh <domain|upn|tenant-id>}"
|
||||
|
||||
# If it looks like a GUID already, pass through.
|
||||
if [[ "$INPUT" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]]; then
|
||||
echo "$INPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# If it's a UPN, strip to domain.
|
||||
DOMAIN="${INPUT#*@}"
|
||||
|
||||
# Lightweight cache keyed by domain.
|
||||
CACHE_DIR="/tmp/remediation-tool/_tenant-cache"
|
||||
mkdir -p "$CACHE_DIR"
|
||||
CACHE_FILE="$CACHE_DIR/${DOMAIN}.txt"
|
||||
if [[ -f "$CACHE_FILE" ]] && [[ $(find "$CACHE_FILE" -mmin -1440 2>/dev/null) ]]; then
|
||||
cat "$CACHE_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# OpenID discovery — parse issuer URL for tenant GUID.
|
||||
RESP=$(curl -s --max-time 10 "https://login.microsoftonline.com/${DOMAIN}/v2.0/.well-known/openid-configuration")
|
||||
TENANT_ID=$(echo "$RESP" | jq -r '.issuer // empty' | sed -E 's|^https://login\.microsoftonline\.com/||;s|/v2\.0/?$||' || true)
|
||||
|
||||
if [[ -z "$TENANT_ID" ]] || [[ ! "$TENANT_ID" =~ ^[0-9a-fA-F]{8}- ]]; then
|
||||
echo "ERROR: could not resolve tenant for domain: $DOMAIN" >&2
|
||||
echo "Response: $RESP" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$TENANT_ID" | tee "$CACHE_FILE"
|
||||
82
.claude/skills/remediation-tool/scripts/tenant-sweep.sh
Executable file
82
.claude/skills/remediation-tool/scripts/tenant-sweep.sh
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tenant-wide signals sweep: failed sign-ins, foreign successful sign-ins, directory audits,
|
||||
# risky users, B2B guest invites, per-user location profile.
|
||||
# Usage: tenant-sweep.sh <tenant-id-or-domain>
|
||||
# Writes raw JSON to /tmp/remediation-tool/{tenant-id}/sweep/
|
||||
# Prints a priority summary to stdout.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
||||
TENANT_INPUT="${1:?usage: tenant-sweep.sh <tenant-id|domain>}"
|
||||
TENANT_ID=$("$SCRIPT_DIR/resolve-tenant.sh" "$TENANT_INPUT")
|
||||
GT=$("$SCRIPT_DIR/get-token.sh" "$TENANT_ID" investigator)
|
||||
|
||||
OUT="/tmp/remediation-tool/$TENANT_ID/sweep"
|
||||
mkdir -p "$OUT"
|
||||
FROM=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
||||
echo "[info] tenant=$TENANT_ID window=30d from=$FROM"
|
||||
|
||||
# Enabled users list
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users?\$top=999&\$filter=accountEnabled%20eq%20true&\$select=id,displayName,userPrincipalName,accountEnabled,userType,externalUserState,lastPasswordChangeDateTime,createdDateTime" \
|
||||
> "$OUT/users.json" &
|
||||
|
||||
# Failed sign-ins tenant-wide
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/auditLogs/signIns?\$filter=(createdDateTime%20ge%20${FROM})%20and%20(status/errorCode%20ne%200)&\$top=999" \
|
||||
> "$OUT/failed_signins.json" &
|
||||
|
||||
# Successful sign-ins tenant-wide (to find non-US)
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/auditLogs/signIns?\$filter=(createdDateTime%20ge%20${FROM})%20and%20(status/errorCode%20eq%200)&\$top=999" \
|
||||
> "$OUT/success_signins.json" &
|
||||
|
||||
# Directory audits, filtered by risky activity names
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?\$filter=activityDateTime%20ge%20${FROM}&\$top=999" \
|
||||
> "$OUT/dir_audits.json" &
|
||||
|
||||
# Risky users (may 403 if IdentityRiskyUser scope absent)
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/identityProtection/riskyUsers?\$top=100" \
|
||||
> "$OUT/risky_users.json" &
|
||||
|
||||
# B2B guest invites
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?\$filter=activityDateTime%20ge%20${FROM}%20and%20activityDisplayName%20eq%20'Invite%20external%20user'&\$top=100" \
|
||||
> "$OUT/guest_invites.json" &
|
||||
|
||||
wait
|
||||
|
||||
echo ""
|
||||
echo "=== Priority 1: accounts with foreign failed sign-ins (credential stuffing candidates) ==="
|
||||
jq '[.value[] | select(.location.countryOrRegion != "US" and .location.countryOrRegion != null) | {user: .userPrincipalName, ip: .ipAddress, country: .location.countryOrRegion, city: .location.city, t: .createdDateTime, err: .status.errorCode, fail: .status.failureReason}] | group_by(.user) | map({user: .[0].user, attempts: length, unique_ips: ([.[]|.ip]|unique|length), countries: ([.[]|.country]|unique), first: ([.[]|.t]|min), last: ([.[]|.t]|max)}) | sort_by(-.attempts)' "$OUT/failed_signins.json"
|
||||
|
||||
echo ""
|
||||
echo "=== Priority 2: successful sign-ins from non-US (suspicious) ==="
|
||||
jq '[.value[] | select(.location.countryOrRegion != "US" and .location.countryOrRegion != null) | {user: .userPrincipalName, ip: .ipAddress, country: .location.countryOrRegion, city: .location.city, t: .createdDateTime, app: .appDisplayName, clientApp: .clientAppUsed}] | sort_by(.t) | reverse | .[:30]' "$OUT/success_signins.json"
|
||||
|
||||
echo ""
|
||||
echo "=== Priority 3: B2B guest invites (30d) ==="
|
||||
jq '[.value[] | {t: .activityDateTime, by: (.initiatedBy.user.userPrincipalName // .initiatedBy.app.displayName), target: [.targetResources[]?|{name: .displayName, upn: .userPrincipalName}], result: .result}] | sort_by(.t) | reverse' "$OUT/guest_invites.json"
|
||||
|
||||
echo ""
|
||||
echo "=== Priority 4: directory audit - consent/role/auth-method changes ==="
|
||||
jq '[.value[] | select(.activityDisplayName | test("[Cc]onsent|[Aa]uthentication [Mm]ethod|Add service principal|Add delegated permission grant|Add app role|Add member to role"; "")) | {t: .activityDateTime, act: .activityDisplayName, by: (.initiatedBy.user.userPrincipalName // .initiatedBy.app.displayName // "system"), target: [.targetResources[]?|{type: .type, name: .displayName, upn: .userPrincipalName}], result: .result}] | sort_by(.t) | reverse | .[:50]' "$OUT/dir_audits.json"
|
||||
|
||||
echo ""
|
||||
echo "=== Risky users (if Identity Protection accessible) ==="
|
||||
if jq -e '.error' "$OUT/risky_users.json" >/dev/null 2>&1; then
|
||||
echo "BLOCKED: $(jq -r '.error.code // "?"' "$OUT/risky_users.json") — $(jq -r '.error.message // ""' "$OUT/risky_users.json")"
|
||||
echo "(Check references/gotchas.md for how to unblock IdentityRiskyUser scope)"
|
||||
else
|
||||
jq '[.value[] | {upn: .userPrincipalName, level: .riskLevel, state: .riskState, detail: .riskDetail, lastUpdated: .riskLastUpdatedDateTime}]' "$OUT/risky_users.json"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== User locations profile (successful sign-ins) ==="
|
||||
jq '[.value[] | {user: .userPrincipalName, country: .location.countryOrRegion, city: .location.city}] | unique | group_by(.user) | map({user: .[0].user, locations: [.[]|{country, city}]|unique})' "$OUT/success_signins.json"
|
||||
|
||||
echo ""
|
||||
echo "[info] Enabled users in tenant: $(jq '.value | length' "$OUT/users.json")"
|
||||
echo "[info] raw artifacts: $OUT"
|
||||
141
.claude/skills/remediation-tool/scripts/user-breach-check.sh
Executable file
141
.claude/skills/remediation-tool/scripts/user-breach-check.sh
Executable file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run the 10-point breach check on a single user.
|
||||
# Usage: user-breach-check.sh <tenant-id-or-domain> <upn>
|
||||
# Writes raw JSON to /tmp/remediation-tool/{tenant-id}/user-breach/{user-slug}/
|
||||
# Prints a summary table to stdout.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
TENANT_INPUT="${1:?usage: user-breach-check.sh <tenant-id|domain> <upn>}"
|
||||
UPN="${2:?usage: user-breach-check.sh <tenant-id|domain> <upn>}"
|
||||
|
||||
TENANT_ID=$("$SCRIPT_DIR/resolve-tenant.sh" "$TENANT_INPUT")
|
||||
GT=$("$SCRIPT_DIR/get-token.sh" "$TENANT_ID" investigator)
|
||||
EXO=$("$SCRIPT_DIR/get-token.sh" "$TENANT_ID" investigator-exo) || EXO=""
|
||||
|
||||
USER_SLUG=$(echo "$UPN" | tr '@.' '__')
|
||||
OUT="/tmp/remediation-tool/$TENANT_ID/user-breach/$USER_SLUG"
|
||||
mkdir -p "$OUT"
|
||||
|
||||
echo "[info] tenant=$TENANT_ID user=$UPN out=$OUT"
|
||||
|
||||
# --- 0. Resolve user object ID ---
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users/${UPN}?\$select=id,displayName,userPrincipalName,mail,accountEnabled,createdDateTime,lastPasswordChangeDateTime" \
|
||||
> "$OUT/00_user.json"
|
||||
UID_=$(jq -r '.id // empty' "$OUT/00_user.json")
|
||||
if [[ -z "$UID_" ]]; then
|
||||
echo "ERROR: user not found or Graph returned error" >&2
|
||||
cat "$OUT/00_user.json" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[info] object id: $UID_"
|
||||
|
||||
# --- 1. Inbox rules (Graph v1.0 — visible only) ---
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users/${UPN}/mailFolders/inbox/messageRules" \
|
||||
> "$OUT/01_inbox_rules_graph.json" &
|
||||
|
||||
# --- 2. Mailbox settings (forwarding flags) ---
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users/${UPN}/mailboxSettings" \
|
||||
> "$OUT/02_mailbox_settings.json" &
|
||||
|
||||
# --- 4. OAuth consents + app role assignments ---
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users/${UPN}/oauth2PermissionGrants" \
|
||||
> "$OUT/04a_oauth_grants.json" &
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users/${UPN}/appRoleAssignments" \
|
||||
> "$OUT/04b_app_role_assignments.json" &
|
||||
|
||||
# --- 5. Authentication methods ---
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users/${UPN}/authentication/methods" \
|
||||
> "$OUT/05_auth_methods.json" &
|
||||
|
||||
wait
|
||||
|
||||
# --- 6. Sign-ins 30d (v1.0 — interactive only) ---
|
||||
FROM=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/auditLogs/signIns?\$filter=userId%20eq%20'${UID_}'%20and%20createdDateTime%20ge%20${FROM}&\$top=200" \
|
||||
> "$OUT/06_signins.json" &
|
||||
|
||||
# --- 7. Directory audits (targetResources = user) 30d ---
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?\$filter=activityDateTime%20ge%20${FROM}%20and%20targetResources/any(t:t/id%20eq%20'${UID_}')&\$top=200" \
|
||||
> "$OUT/07_dir_audits.json" &
|
||||
|
||||
# --- 8. Risky user + risk detections (403 if app lacks IdentityRiskyUser scope) ---
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/identityProtection/riskyUsers/${UID_}" \
|
||||
> "$OUT/08a_risky_user.json" &
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/identityProtection/riskDetections?\$filter=userId%20eq%20'${UID_}'&\$top=100" \
|
||||
> "$OUT/08b_risk_detections.json" &
|
||||
|
||||
# --- 9. Sent items (last 25) ---
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users/${UPN}/mailFolders/sentitems/messages?\$top=25&\$orderby=sentDateTime%20desc&\$select=subject,toRecipients,sentDateTime,from" \
|
||||
> "$OUT/09_sent.json" &
|
||||
|
||||
# --- 10. Deleted items (last 25) ---
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users/${UPN}/mailFolders/deleteditems/messages?\$top=25&\$orderby=receivedDateTime%20desc&\$select=subject,from,receivedDateTime" \
|
||||
> "$OUT/10_deleted.json" &
|
||||
|
||||
wait
|
||||
|
||||
# --- 3. Exchange REST (hidden rules + delegates + SendAs + Get-Mailbox) ---
|
||||
if [[ -n "$EXO" ]]; then
|
||||
EX_URL="https://outlook.office365.com/adminapi/beta/${TENANT_ID}/InvokeCommand"
|
||||
|
||||
curl -s -X POST -H "Authorization: Bearer $EXO" -H "Content-Type: application/json" "$EX_URL" \
|
||||
-d "{\"CmdletInput\":{\"CmdletName\":\"Get-InboxRule\",\"Parameters\":{\"Mailbox\":\"${UPN}\",\"IncludeHidden\":true}}}" \
|
||||
> "$OUT/03a_InboxRule_hidden.json" &
|
||||
|
||||
curl -s -X POST -H "Authorization: Bearer $EXO" -H "Content-Type: application/json" "$EX_URL" \
|
||||
-d "{\"CmdletInput\":{\"CmdletName\":\"Get-MailboxPermission\",\"Parameters\":{\"Identity\":\"${UPN}\"}}}" \
|
||||
> "$OUT/03b_MailboxPermission.json" &
|
||||
|
||||
curl -s -X POST -H "Authorization: Bearer $EXO" -H "Content-Type: application/json" "$EX_URL" \
|
||||
-d "{\"CmdletInput\":{\"CmdletName\":\"Get-RecipientPermission\",\"Parameters\":{\"Identity\":\"${UPN}\"}}}" \
|
||||
> "$OUT/03c_RecipientPermission.json" &
|
||||
|
||||
curl -s -X POST -H "Authorization: Bearer $EXO" -H "Content-Type: application/json" "$EX_URL" \
|
||||
-d "{\"CmdletInput\":{\"CmdletName\":\"Get-Mailbox\",\"Parameters\":{\"Identity\":\"${UPN}\"}}}" \
|
||||
> "$OUT/03d_Mailbox.json" &
|
||||
|
||||
wait
|
||||
else
|
||||
echo "[warn] no Exchange token; skipping check 3 (hidden rules/delegates/SendAs/mailbox forwarding flags)"
|
||||
fi
|
||||
|
||||
# --- Summary table ---
|
||||
echo ""
|
||||
echo "=== Summary: $UPN ==="
|
||||
jq -r '"account_enabled: \(.accountEnabled) lastPwChange: \(.lastPasswordChangeDateTime) created: \(.createdDateTime)"' "$OUT/00_user.json"
|
||||
echo "01 inbox_rules (Graph): $(jq '.value | length // "error"' "$OUT/01_inbox_rules_graph.json")"
|
||||
echo "02 forwarding: fwdSmtp=$(jq -r '.automaticRepliesSetting.status // "n/a"' "$OUT/02_mailbox_settings.json" 2>/dev/null) (see mailbox Get-Mailbox for forwarding fields)"
|
||||
echo "04a oauth_grants: $(jq '.value | length // "error"' "$OUT/04a_oauth_grants.json")"
|
||||
echo "04b app_role_assignments: $(jq '.value | length // "error"' "$OUT/04b_app_role_assignments.json")"
|
||||
echo "05 auth_methods: $(jq '.value | length // "error"' "$OUT/05_auth_methods.json")"
|
||||
echo "06 signins (30d, interactive): $(jq '.value | length // "error"' "$OUT/06_signins.json") non-US: $(jq '[.value[]?|select(.location.countryOrRegion != "US" and .location.countryOrRegion != null)] | length' "$OUT/06_signins.json" 2>/dev/null)"
|
||||
echo "07 dir_audits (30d): $(jq '.value | length // "error"' "$OUT/07_dir_audits.json")"
|
||||
echo "08 risky_user: $(jq -r '.riskLevel // .error.code // "none"' "$OUT/08a_risky_user.json" 2>/dev/null)"
|
||||
echo "08 risk_detections: $(jq '.value | length // (.error.code // "error")' "$OUT/08b_risk_detections.json")"
|
||||
echo "09 sent (recent 25): $(jq '.value | length // "error"' "$OUT/09_sent.json")"
|
||||
echo "10 deleted (recent 25): $(jq '.value | length // "error"' "$OUT/10_deleted.json")"
|
||||
if [[ -f "$OUT/03a_InboxRule_hidden.json" ]]; then
|
||||
HIDDEN=$(jq '.value | length // (.error.code // "?")' "$OUT/03a_InboxRule_hidden.json" 2>/dev/null || echo "?")
|
||||
echo "03a hidden_inbox_rules: $HIDDEN"
|
||||
echo "03b mailbox_permissions: $(jq '[.value[]? | select(.User != "NT AUTHORITY\\SELF")] | length // "?"' "$OUT/03b_MailboxPermission.json" 2>/dev/null) non-SELF"
|
||||
echo "03c send_as: $(jq '[.value[]? | select(.Trustee != "NT AUTHORITY\\SELF")] | length // "?"' "$OUT/03c_RecipientPermission.json" 2>/dev/null) non-SELF"
|
||||
echo "03d mailbox_forwarding: fwdAddr=$(jq -r '.value[0].ForwardingAddress // "null"' "$OUT/03d_Mailbox.json" 2>/dev/null) fwdSmtp=$(jq -r '.value[0].ForwardingSmtpAddress // "null"' "$OUT/03d_Mailbox.json" 2>/dev/null)"
|
||||
else
|
||||
echo "03 exchange_rest: SKIPPED (no exchange token — tenant likely needs Exchange Admin role assigned)"
|
||||
fi
|
||||
echo ""
|
||||
echo "[info] raw artifacts: $OUT"
|
||||
75
.claude/skills/remediation-tool/templates/breach-report.md
Normal file
75
.claude/skills/remediation-tool/templates/breach-report.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# {{TITLE}}
|
||||
|
||||
**Date:** {{YYYY-MM-DD}}
|
||||
**Tenant:** {{tenant-display-name}} ({{domain}}, {{tenant-id}})
|
||||
**Subject:** {{user-or-tenant}}
|
||||
**Tool:** Claude-MSP-Access / ComputerGuru - AI Remediation (App ID `fabb3421-8b34-484b-bc17-e46de9703418`)
|
||||
**Scope:** {{read-only | included remediation}}
|
||||
|
||||
## Summary
|
||||
|
||||
- {{3-5 bullets: breach indicators found? which categories? priority actions?}}
|
||||
|
||||
## Target details
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| UPN | |
|
||||
| Object ID | |
|
||||
| Account Enabled | |
|
||||
| Created | |
|
||||
| Last Password Change | |
|
||||
|
||||
## Per-check findings
|
||||
|
||||
### 1. Inbox rules (Graph)
|
||||
{{count, flagged items verbatim}}
|
||||
|
||||
### 2. Mailbox forwarding / settings
|
||||
{{forwarding flags, auto-reply status}}
|
||||
|
||||
### 3. Exchange REST (hidden rules, delegates, SendAs, Get-Mailbox)
|
||||
{{hidden rule count, non-SELF permissions, ForwardingAddress/ForwardingSmtpAddress}}
|
||||
|
||||
### 4. OAuth consents + app role assignments
|
||||
{{apps consented, when, scopes}}
|
||||
|
||||
### 5. Authentication methods
|
||||
{{methods, creation dates — flag any inside attack window}}
|
||||
|
||||
### 6. Sign-ins (30d)
|
||||
{{count, unique IPs, countries, failures — flag non-US and legacy client apps}}
|
||||
|
||||
### 7. Directory audits
|
||||
{{30d changes targeting user, by-whom analysis}}
|
||||
|
||||
### 8. Risky users / risk detections
|
||||
{{risk level, recent detections — or note if blocked by missing permission}}
|
||||
|
||||
### 9. Sent items (recent 25)
|
||||
{{sample of recipients/subjects — flag blast patterns or unusual externals}}
|
||||
|
||||
### 10. Deleted items (recent 25)
|
||||
{{sample — flag deleted security alerts or MFA notifications}}
|
||||
|
||||
## Suspicious items (pulled out of per-check findings)
|
||||
|
||||
{{bullets for anything abnormal — external forwards, hidden rules, unfamiliar consents, foreign-geo sign-ins, new auth methods within attack window}}
|
||||
|
||||
## Gaps — checks not completed
|
||||
|
||||
{{list any 403s or missing permissions with exact remediation link (see gotchas.md)}}
|
||||
|
||||
## Next actions
|
||||
|
||||
1. {{specific action + owner + deadline}}
|
||||
2. {{...}}
|
||||
|
||||
## Remediation actions (if any)
|
||||
|
||||
{{populated only when `/remediation-tool remediate` was executed — include cmdlet, parameters, response, timestamp}}
|
||||
|
||||
## Data artifacts
|
||||
|
||||
Raw JSON saved at `/tmp/remediation-tool/{{tenant-id}}/{{check-dir}}/` — files:
|
||||
- {{list filenames the scripts produced}}
|
||||
117
.claude/skills/skill-creator/SKILL.md
Normal file
117
.claude/skills/skill-creator/SKILL.md
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
name: skill-creator
|
||||
description: |
|
||||
Create new Claude Code custom skills and slash commands. Use when the user wants to create a new skill,
|
||||
add a slash command, build a custom command, or set up a new automation. Guides through the process of
|
||||
defining the skill's purpose, triggers, and implementation, then generates the proper file structure.
|
||||
---
|
||||
|
||||
# Skill Creator
|
||||
|
||||
You help the user create new Claude Code custom skills and slash commands.
|
||||
|
||||
## Two Types of Custom Extensions
|
||||
|
||||
### 1. Skills (`.claude/skills/{name}/SKILL.md`)
|
||||
- Rich, multi-purpose capabilities with automatic invocation triggers
|
||||
- Can include supporting files (scripts, references, checklists)
|
||||
- Best for: complex behaviors, design patterns, validation workflows, integrations
|
||||
|
||||
### 2. Slash Commands (`.claude/commands/{name}.md`)
|
||||
- Simple, user-invoked commands triggered by `/{name}`
|
||||
- Single markdown file with instructions
|
||||
- Best for: workflows the user explicitly triggers, task automation, shortcuts
|
||||
- Can accept arguments via `$ARGUMENTS`
|
||||
|
||||
## Creation Process
|
||||
|
||||
### Step 1: Gather Requirements
|
||||
|
||||
Ask the user:
|
||||
1. **What should this skill/command do?** (core purpose)
|
||||
2. **Skill or command?** Help them decide:
|
||||
- If it should run automatically in response to certain actions -> **Skill**
|
||||
- If the user will invoke it explicitly with `/{name}` -> **Command**
|
||||
- If unsure, recommend based on the use case
|
||||
3. **Name** - short, kebab-case identifier (e.g., `code-review`, `deploy-check`)
|
||||
4. **When should it trigger?** (for skills: automatic triggers; for commands: typical usage)
|
||||
|
||||
### Step 2: Generate the Files
|
||||
|
||||
#### For Skills
|
||||
|
||||
Create `.claude/skills/{name}/SKILL.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: {name}
|
||||
description: |
|
||||
{Detailed description. This is used for discovery/matching, so be specific about
|
||||
when this skill should be invoked. Include trigger keywords and example scenarios.}
|
||||
---
|
||||
|
||||
# {Skill Title}
|
||||
|
||||
{Clear instructions for what Claude should do when this skill is invoked.}
|
||||
|
||||
## When to Invoke
|
||||
|
||||
{List specific triggers - file types, actions, keywords that should activate this skill.}
|
||||
|
||||
## Workflow
|
||||
|
||||
{Step-by-step process the skill follows.}
|
||||
|
||||
## Guidelines
|
||||
|
||||
{Rules, patterns, and best practices to follow.}
|
||||
```
|
||||
|
||||
#### For Commands
|
||||
|
||||
Create `.claude/commands/{name}.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: {One-line description shown in command list}
|
||||
---
|
||||
|
||||
# {Command Title}
|
||||
|
||||
{Instructions for what Claude should do when the user runs /{name}.}
|
||||
|
||||
## Arguments
|
||||
|
||||
If the command accepts arguments, reference them via `$ARGUMENTS`.
|
||||
|
||||
## Workflow
|
||||
|
||||
{Step-by-step process.}
|
||||
```
|
||||
|
||||
### Step 3: Register and Validate
|
||||
|
||||
After creating the files:
|
||||
1. Confirm the file was created in the correct location
|
||||
2. Tell the user they can invoke it:
|
||||
- Skills: Explain the automatic triggers or manual invocation via `/skill-name`
|
||||
- Commands: Tell them to use `/{name}` or `/{name} arguments`
|
||||
3. Remind them to update CLAUDE.md's Commands & Skills table if they want it documented there
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
Before finalizing, verify:
|
||||
- [ ] Description is detailed enough for Claude to match it to relevant situations
|
||||
- [ ] Instructions are clear and actionable (Claude will follow them literally)
|
||||
- [ ] The skill/command doesn't duplicate an existing one
|
||||
- [ ] File is in the correct location (`.claude/skills/` or `.claude/commands/`)
|
||||
- [ ] Name uses kebab-case and is concise
|
||||
- [ ] For skills with auto-triggers: triggers are specific enough to avoid false positives
|
||||
|
||||
## Tips for Good Skills/Commands
|
||||
|
||||
- **Be specific in descriptions** - vague descriptions lead to missed or false invocations
|
||||
- **Include examples** in the instructions so Claude understands edge cases
|
||||
- **Keep scope focused** - one skill per concern, don't create mega-skills
|
||||
- **Test after creation** - have the user try invoking it to verify behavior
|
||||
- **Reference existing patterns** - look at `.claude/skills/` and `.claude/commands/` for examples
|
||||
105
.claude/skills/stop-slop/SKILL.md
Normal file
105
.claude/skills/stop-slop/SKILL.md
Normal file
@@ -0,0 +1,105 @@
|
||||
---
|
||||
name: stop-slop
|
||||
description: |
|
||||
Enforce high-quality, slop-free output in all Claude responses. MANDATORY AUTOMATIC INVOCATION:
|
||||
This skill is always active. It governs how Claude writes text, code comments, commit messages,
|
||||
documentation, and any other output. Detects and eliminates generic AI filler, hollow phrases,
|
||||
unnecessary verbosity, and performative enthusiasm. Applies to all output — conversation, code,
|
||||
docs, and generated content.
|
||||
---
|
||||
|
||||
# Stop Slop
|
||||
|
||||
You are a direct, competent engineer. Write like one. Every word must earn its place.
|
||||
|
||||
## Always-On Rules
|
||||
|
||||
These rules apply to ALL output at ALL times. No exceptions.
|
||||
|
||||
### Banned Patterns -- Never Write These
|
||||
|
||||
**Performative enthusiasm and filler openers:**
|
||||
- "Great question!", "Excellent point!", "That's a really interesting..."
|
||||
- "Certainly!", "Absolutely!", "Of course!", "Sure thing!"
|
||||
- "I'd be happy to help!", "Let me help you with that!"
|
||||
- "Good news!", "Here's the exciting part..."
|
||||
|
||||
**Hollow transitions and hedging:**
|
||||
- "It's worth noting that..." (just state it)
|
||||
- "It's important to remember..." (just state it)
|
||||
- "As you can see..." / "As we discussed..."
|
||||
- "Basically..." / "Essentially..." / "Fundamentally..."
|
||||
- "In order to..." (use "to")
|
||||
- "It should be noted that..." (just note it)
|
||||
- "At the end of the day..."
|
||||
- "Moving forward..."
|
||||
|
||||
**Unnecessary meta-commentary:**
|
||||
- "Let me explain..." (just explain)
|
||||
- "I'll now..." / "Next, I'll..." (just do it)
|
||||
- "Here's what I found..." (just show it)
|
||||
- "Let me break this down..." (just break it down)
|
||||
|
||||
**Trailing summaries and sign-offs:**
|
||||
- Restating what was just done at the end of a response
|
||||
- "Let me know if you have any questions!"
|
||||
- "Hope this helps!"
|
||||
- "Feel free to ask if you need anything else!"
|
||||
- "Happy coding!" / "Happy hacking!"
|
||||
- Any variation of "don't hesitate to reach out"
|
||||
|
||||
**Weasel words and padding:**
|
||||
- "Very", "really", "quite", "rather", "somewhat", "fairly"
|
||||
- "Just" (when used as filler, not as "only")
|
||||
- "Simply" (when the thing isn't simple, or as filler)
|
||||
- "Actually" (at start of sentences, as filler)
|
||||
- "Obviously" / "Clearly" (if it were obvious, you wouldn't say it)
|
||||
|
||||
**Sycophantic agreement:**
|
||||
- "You're absolutely right that..."
|
||||
- "That's a great approach!"
|
||||
- "What a thoughtful question!"
|
||||
- Praising the user's code/ideas before giving feedback
|
||||
|
||||
### Writing Standards
|
||||
|
||||
**Lead with the answer.** Don't build up to it. State the conclusion, then support it if needed.
|
||||
|
||||
**One sentence beats three.** If you can say it shorter, do. Compress ruthlessly.
|
||||
|
||||
**No preamble.** Start with the substance. Drop throat-clearing intros.
|
||||
|
||||
**No postamble.** End when the content ends. Don't summarize what you just said. Don't offer further help.
|
||||
|
||||
**Be specific.** "This fails because X" not "There might be some issues with this approach."
|
||||
|
||||
**Code comments: only when non-obvious.** Don't add comments that restate what the code does. Comment the *why*, not the *what*. Most code needs zero comments.
|
||||
|
||||
**Commit messages: state the change.** Not "This commit updates the..." -- just "Update X to handle Y."
|
||||
|
||||
**Error messages: state what went wrong and what to do.** Not "Oops! It looks like something went wrong."
|
||||
|
||||
### Calibration Examples
|
||||
|
||||
**Slop:**
|
||||
> Great question! Let me help you with that. So basically, what's happening here is that the function is essentially trying to parse the input string. It's worth noting that this can sometimes fail if the input isn't valid JSON. I'd recommend wrapping it in a try-catch block to handle any potential errors that might occur. Let me know if you have any questions!
|
||||
|
||||
**Clean:**
|
||||
> The function fails on invalid JSON. Wrap it in try-catch:
|
||||
> ```js
|
||||
> try { return JSON.parse(input); } catch { return null; }
|
||||
> ```
|
||||
|
||||
**Slop:**
|
||||
> I've successfully updated the configuration file to include the new database connection settings. The changes include adding the host, port, username, and password fields as requested. Everything should be working correctly now. Feel free to test it out and let me know if you run into any issues!
|
||||
|
||||
**Clean:**
|
||||
> Updated the database config with the new connection settings.
|
||||
|
||||
### What This Skill Does NOT Do
|
||||
|
||||
- It does not make responses terse to the point of being unhelpful
|
||||
- It does not remove necessary technical explanation
|
||||
- It does not prevent friendly, human tone -- just fake enthusiasm
|
||||
- It does not restrict response length when length is warranted by complexity
|
||||
- Thoroughness is good. Fluff is not. Know the difference.
|
||||
202
.claude/skills/theme-factory/LICENSE.txt
Normal file
202
.claude/skills/theme-factory/LICENSE.txt
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
59
.claude/skills/theme-factory/SKILL.md
Normal file
59
.claude/skills/theme-factory/SKILL.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: theme-factory
|
||||
description: Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
|
||||
# Theme Factory Skill
|
||||
|
||||
This skill provides a curated collection of professional font and color themes themes, each with carefully selected color palettes and font pairings. Once a theme is chosen, it can be applied to any artifact.
|
||||
|
||||
## Purpose
|
||||
|
||||
To apply consistent, professional styling to presentation slide decks, use this skill. Each theme includes:
|
||||
- A cohesive color palette with hex codes
|
||||
- Complementary font pairings for headers and body text
|
||||
- A distinct visual identity suitable for different contexts and audiences
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
To apply styling to a slide deck or other artifact:
|
||||
|
||||
1. **Show the theme showcase**: Display the `theme-showcase.pdf` file to allow users to see all available themes visually. Do not make any modifications to it; simply show the file for viewing.
|
||||
2. **Ask for their choice**: Ask which theme to apply to the deck
|
||||
3. **Wait for selection**: Get explicit confirmation about the chosen theme
|
||||
4. **Apply the theme**: Once a theme has been chosen, apply the selected theme's colors and fonts to the deck/artifact
|
||||
|
||||
## Themes Available
|
||||
|
||||
The following 10 themes are available, each showcased in `theme-showcase.pdf`:
|
||||
|
||||
1. **Ocean Depths** - Professional and calming maritime theme
|
||||
2. **Sunset Boulevard** - Warm and vibrant sunset colors
|
||||
3. **Forest Canopy** - Natural and grounded earth tones
|
||||
4. **Modern Minimalist** - Clean and contemporary grayscale
|
||||
5. **Golden Hour** - Rich and warm autumnal palette
|
||||
6. **Arctic Frost** - Cool and crisp winter-inspired theme
|
||||
7. **Desert Rose** - Soft and sophisticated dusty tones
|
||||
8. **Tech Innovation** - Bold and modern tech aesthetic
|
||||
9. **Botanical Garden** - Fresh and organic garden colors
|
||||
10. **Midnight Galaxy** - Dramatic and cosmic deep tones
|
||||
|
||||
## Theme Details
|
||||
|
||||
Each theme is defined in the `themes/` directory with complete specifications including:
|
||||
- Cohesive color palette with hex codes
|
||||
- Complementary font pairings for headers and body text
|
||||
- Distinct visual identity suitable for different contexts and audiences
|
||||
|
||||
## Application Process
|
||||
|
||||
After a preferred theme is selected:
|
||||
1. Read the corresponding theme file from the `themes/` directory
|
||||
2. Apply the specified colors and fonts consistently throughout the deck
|
||||
3. Ensure proper contrast and readability
|
||||
4. Maintain the theme's visual identity across all slides
|
||||
|
||||
## Create your Own Theme
|
||||
To handle cases where none of the existing themes work for an artifact, create a custom theme. Based on provided inputs, generate a new theme similar to the ones above. Give the theme a similar name describing what the font/color combinations represent. Use any basic description provided to choose appropriate colors/fonts. After generating the theme, show it for review and verification. Following that, apply the theme as described above.
|
||||
BIN
.claude/skills/theme-factory/theme-showcase.pdf
Normal file
BIN
.claude/skills/theme-factory/theme-showcase.pdf
Normal file
Binary file not shown.
19
.claude/skills/theme-factory/themes/arctic-frost.md
Normal file
19
.claude/skills/theme-factory/themes/arctic-frost.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Arctic Frost
|
||||
|
||||
A cool and crisp winter-inspired theme that conveys clarity, precision, and professionalism.
|
||||
|
||||
## Color Palette
|
||||
|
||||
- **Ice Blue**: `#d4e4f7` - Light backgrounds and highlights
|
||||
- **Steel Blue**: `#4a6fa5` - Primary accent color
|
||||
- **Silver**: `#c0c0c0` - Metallic accent elements
|
||||
- **Crisp White**: `#fafafa` - Clean backgrounds and text
|
||||
|
||||
## Typography
|
||||
|
||||
- **Headers**: DejaVu Sans Bold
|
||||
- **Body Text**: DejaVu Sans
|
||||
|
||||
## Best Used For
|
||||
|
||||
Healthcare presentations, technology solutions, winter sports, clean tech, pharmaceutical content.
|
||||
19
.claude/skills/theme-factory/themes/botanical-garden.md
Normal file
19
.claude/skills/theme-factory/themes/botanical-garden.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Botanical Garden
|
||||
|
||||
A fresh and organic theme featuring vibrant garden-inspired colors for lively presentations.
|
||||
|
||||
## Color Palette
|
||||
|
||||
- **Fern Green**: `#4a7c59` - Rich natural green
|
||||
- **Marigold**: `#f9a620` - Bright floral accent
|
||||
- **Terracotta**: `#b7472a` - Earthy warm tone
|
||||
- **Cream**: `#f5f3ed` - Soft neutral backgrounds
|
||||
|
||||
## Typography
|
||||
|
||||
- **Headers**: DejaVu Serif Bold
|
||||
- **Body Text**: DejaVu Sans
|
||||
|
||||
## Best Used For
|
||||
|
||||
Garden centers, food presentations, farm-to-table content, botanical brands, natural products.
|
||||
19
.claude/skills/theme-factory/themes/desert-rose.md
Normal file
19
.claude/skills/theme-factory/themes/desert-rose.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Desert Rose
|
||||
|
||||
A soft and sophisticated theme with dusty, muted tones perfect for elegant presentations.
|
||||
|
||||
## Color Palette
|
||||
|
||||
- **Dusty Rose**: `#d4a5a5` - Soft primary color
|
||||
- **Clay**: `#b87d6d` - Earthy accent
|
||||
- **Sand**: `#e8d5c4` - Warm neutral backgrounds
|
||||
- **Deep Burgundy**: `#5d2e46` - Rich dark contrast
|
||||
|
||||
## Typography
|
||||
|
||||
- **Headers**: FreeSans Bold
|
||||
- **Body Text**: FreeSans
|
||||
|
||||
## Best Used For
|
||||
|
||||
Fashion presentations, beauty brands, wedding planning, interior design, boutique businesses.
|
||||
19
.claude/skills/theme-factory/themes/forest-canopy.md
Normal file
19
.claude/skills/theme-factory/themes/forest-canopy.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Forest Canopy
|
||||
|
||||
A natural and grounded theme featuring earth tones inspired by dense forest environments.
|
||||
|
||||
## Color Palette
|
||||
|
||||
- **Forest Green**: `#2d4a2b` - Primary dark green
|
||||
- **Sage**: `#7d8471` - Muted green accent
|
||||
- **Olive**: `#a4ac86` - Light accent color
|
||||
- **Ivory**: `#faf9f6` - Backgrounds and text
|
||||
|
||||
## Typography
|
||||
|
||||
- **Headers**: FreeSerif Bold
|
||||
- **Body Text**: FreeSans
|
||||
|
||||
## Best Used For
|
||||
|
||||
Environmental presentations, sustainability reports, outdoor brands, wellness content, organic products.
|
||||
19
.claude/skills/theme-factory/themes/golden-hour.md
Normal file
19
.claude/skills/theme-factory/themes/golden-hour.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Golden Hour
|
||||
|
||||
A rich and warm autumnal palette that creates an inviting and sophisticated atmosphere.
|
||||
|
||||
## Color Palette
|
||||
|
||||
- **Mustard Yellow**: `#f4a900` - Bold primary accent
|
||||
- **Terracotta**: `#c1666b` - Warm secondary color
|
||||
- **Warm Beige**: `#d4b896` - Neutral backgrounds
|
||||
- **Chocolate Brown**: `#4a403a` - Dark text and anchors
|
||||
|
||||
## Typography
|
||||
|
||||
- **Headers**: FreeSans Bold
|
||||
- **Body Text**: FreeSans
|
||||
|
||||
## Best Used For
|
||||
|
||||
Restaurant presentations, hospitality brands, fall campaigns, cozy lifestyle content, artisan products.
|
||||
19
.claude/skills/theme-factory/themes/midnight-galaxy.md
Normal file
19
.claude/skills/theme-factory/themes/midnight-galaxy.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Midnight Galaxy
|
||||
|
||||
A dramatic and cosmic theme with deep purples and mystical tones for impactful presentations.
|
||||
|
||||
## Color Palette
|
||||
|
||||
- **Deep Purple**: `#2b1e3e` - Rich dark base
|
||||
- **Cosmic Blue**: `#4a4e8f` - Mystical mid-tone
|
||||
- **Lavender**: `#a490c2` - Soft accent color
|
||||
- **Silver**: `#e6e6fa` - Light highlights and text
|
||||
|
||||
## Typography
|
||||
|
||||
- **Headers**: FreeSans Bold
|
||||
- **Body Text**: FreeSans
|
||||
|
||||
## Best Used For
|
||||
|
||||
Entertainment industry, gaming presentations, nightlife venues, luxury brands, creative agencies.
|
||||
19
.claude/skills/theme-factory/themes/modern-minimalist.md
Normal file
19
.claude/skills/theme-factory/themes/modern-minimalist.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Modern Minimalist
|
||||
|
||||
A clean and contemporary theme with a sophisticated grayscale palette for maximum versatility.
|
||||
|
||||
## Color Palette
|
||||
|
||||
- **Charcoal**: `#36454f` - Primary dark color
|
||||
- **Slate Gray**: `#708090` - Medium gray for accents
|
||||
- **Light Gray**: `#d3d3d3` - Backgrounds and dividers
|
||||
- **White**: `#ffffff` - Text and clean backgrounds
|
||||
|
||||
## Typography
|
||||
|
||||
- **Headers**: DejaVu Sans Bold
|
||||
- **Body Text**: DejaVu Sans
|
||||
|
||||
## Best Used For
|
||||
|
||||
Tech presentations, architecture portfolios, design showcases, modern business proposals, data visualization.
|
||||
19
.claude/skills/theme-factory/themes/ocean-depths.md
Normal file
19
.claude/skills/theme-factory/themes/ocean-depths.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Ocean Depths
|
||||
|
||||
A professional and calming maritime theme that evokes the serenity of deep ocean waters.
|
||||
|
||||
## Color Palette
|
||||
|
||||
- **Deep Navy**: `#1a2332` - Primary background color
|
||||
- **Teal**: `#2d8b8b` - Accent color for highlights and emphasis
|
||||
- **Seafoam**: `#a8dadc` - Secondary accent for lighter elements
|
||||
- **Cream**: `#f1faee` - Text and light backgrounds
|
||||
|
||||
## Typography
|
||||
|
||||
- **Headers**: DejaVu Sans Bold
|
||||
- **Body Text**: DejaVu Sans
|
||||
|
||||
## Best Used For
|
||||
|
||||
Corporate presentations, financial reports, professional consulting decks, trust-building content.
|
||||
19
.claude/skills/theme-factory/themes/sunset-boulevard.md
Normal file
19
.claude/skills/theme-factory/themes/sunset-boulevard.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Sunset Boulevard
|
||||
|
||||
A warm and vibrant theme inspired by golden hour sunsets, perfect for energetic and creative presentations.
|
||||
|
||||
## Color Palette
|
||||
|
||||
- **Burnt Orange**: `#e76f51` - Primary accent color
|
||||
- **Coral**: `#f4a261` - Secondary warm accent
|
||||
- **Warm Sand**: `#e9c46a` - Highlighting and backgrounds
|
||||
- **Deep Purple**: `#264653` - Dark contrast and text
|
||||
|
||||
## Typography
|
||||
|
||||
- **Headers**: DejaVu Serif Bold
|
||||
- **Body Text**: DejaVu Sans
|
||||
|
||||
## Best Used For
|
||||
|
||||
Creative pitches, marketing presentations, lifestyle brands, event promotions, inspirational content.
|
||||
19
.claude/skills/theme-factory/themes/tech-innovation.md
Normal file
19
.claude/skills/theme-factory/themes/tech-innovation.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Tech Innovation
|
||||
|
||||
A bold and modern theme with high-contrast colors perfect for cutting-edge technology presentations.
|
||||
|
||||
## Color Palette
|
||||
|
||||
- **Electric Blue**: `#0066ff` - Vibrant primary accent
|
||||
- **Neon Cyan**: `#00ffff` - Bright highlight color
|
||||
- **Dark Gray**: `#1e1e1e` - Deep backgrounds
|
||||
- **White**: `#ffffff` - Clean text and contrast
|
||||
|
||||
## Typography
|
||||
|
||||
- **Headers**: DejaVu Sans Bold
|
||||
- **Body Text**: DejaVu Sans
|
||||
|
||||
## Best Used For
|
||||
|
||||
Tech startups, software launches, innovation showcases, AI/ML presentations, digital transformation content.
|
||||
29
.claude/users.json
Normal file
29
.claude/users.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"users": {
|
||||
"mike": {
|
||||
"full_name": "Mike Swanson",
|
||||
"email": "mike@azcomputerguru.com",
|
||||
"role": "admin",
|
||||
"title": "President",
|
||||
"known_machines": ["DESKTOP-0O8A1RL", "Mikes-MacBook-Air"],
|
||||
"git_name": "Mike Swanson",
|
||||
"git_email": "mike@azcomputerguru.com",
|
||||
"notes": "Owner. Full access to everything."
|
||||
},
|
||||
"howard": {
|
||||
"full_name": "Howard Enos",
|
||||
"email": "howard@azcomputerguru.com",
|
||||
"role": "tech",
|
||||
"title": "Technician",
|
||||
"known_machines": ["ACG-TECH03L", "Howard-Home"],
|
||||
"git_name": "Howard Enos",
|
||||
"git_email": "howard@azcomputerguru.com",
|
||||
"gitea_username": "howard",
|
||||
"notes": "Employee, Mike's brother. Full trust. Same access as Mike for MSP tracking and daily work. Has own Gitea account (howard) with admin access to all repos. Password rotated 2026-04-21 — stored in Howard's 1Password, not in this file."
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"admin": "Full access to all systems, credentials, deployments, and infrastructure.",
|
||||
"tech": "Full access to all systems, credentials, and client work. Same as admin for this organization."
|
||||
}
|
||||
}
|
||||
168
.claude/vault-setup-mac.md
Normal file
168
.claude/vault-setup-mac.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Vault Setup on Mac (Mikes-MacBook-Air.local)
|
||||
|
||||
**Status:** Blocked on authentication
|
||||
**Created:** 2026-04-21
|
||||
**Purpose:** Enable remediation-tool SOPS credential access on Mac
|
||||
|
||||
---
|
||||
|
||||
## Current State
|
||||
|
||||
**Vault repo:** NOT cloned on this Mac
|
||||
**identity.json:** Missing `vault_path` field
|
||||
**Remediation-tool:** Cannot acquire tokens (no vault access)
|
||||
|
||||
---
|
||||
|
||||
## What's Needed
|
||||
|
||||
### Step 1: Clone Vault Repository
|
||||
|
||||
**Vault URL:** `http://172.16.3.20:3000/azcomputerguru/vault.git`
|
||||
|
||||
**Authentication required.** Options:
|
||||
|
||||
**Option A: Use Gitea credentials (interactive)**
|
||||
```bash
|
||||
git clone http://azcomputerguru@172.16.3.20:3000/azcomputerguru/vault.git ~/vault
|
||||
# Will prompt for password
|
||||
```
|
||||
|
||||
**Option B: Use stored credentials**
|
||||
If you have git credential helper configured:
|
||||
```bash
|
||||
git config --global credential.helper osxkeychain
|
||||
git clone http://azcomputerguru@172.16.3.20:3000/azcomputerguru/vault.git ~/vault
|
||||
```
|
||||
|
||||
**Option C: Use SSH (if keys configured)**
|
||||
```bash
|
||||
git clone git@172.16.3.20:azcomputerguru/vault.git ~/vault
|
||||
```
|
||||
|
||||
### Step 2: Add vault_path to identity.json
|
||||
|
||||
**File:** `/Users/azcomputerguru/ClaudeTools/.claude/identity.json`
|
||||
|
||||
**Add this field:**
|
||||
```json
|
||||
{
|
||||
"user": "mike",
|
||||
"full_name": "Mike Swanson",
|
||||
"email": "mike@azcomputerguru.com",
|
||||
"role": "admin",
|
||||
"machine": "Mikes-MacBook-Air",
|
||||
"mode": "general",
|
||||
"last_updated": "2026-04-19T08:40:00Z",
|
||||
"vault_path": "/Users/azcomputerguru/vault"
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Verify SOPS Files Are Present
|
||||
|
||||
```bash
|
||||
ls -la ~/vault/msp-tools/computerguru-*.sops.yaml
|
||||
```
|
||||
|
||||
**Expected: 5 files**
|
||||
- computerguru-security-investigator.sops.yaml
|
||||
- computerguru-exchange-operator.sops.yaml
|
||||
- computerguru-user-manager.sops.yaml
|
||||
- computerguru-tenant-admin.sops.yaml
|
||||
- computerguru-defender-addon.sops.yaml
|
||||
|
||||
### Step 4: Configure SOPS
|
||||
|
||||
**Check if age key exists:**
|
||||
```bash
|
||||
test -f ~/.config/sops/age/keys.txt && echo "Age key exists" || echo "Need age key"
|
||||
```
|
||||
|
||||
**If age key is missing:**
|
||||
You'll need the SOPS age private key from DESKTOP-0O8A1RL or ACG-Tech03L.
|
||||
|
||||
**Location on Windows:** `C:\Users\<username>\.config\sops\age\keys.txt`
|
||||
|
||||
Copy the private key to Mac:
|
||||
```bash
|
||||
mkdir -p ~/.config/sops/age
|
||||
# Copy keys.txt content to ~/.config/sops/age/keys.txt
|
||||
chmod 600 ~/.config/sops/age/keys.txt
|
||||
```
|
||||
|
||||
### Step 5: Test Token Acquisition
|
||||
|
||||
```bash
|
||||
cd /Users/azcomputerguru/ClaudeTools/.claude/skills/remediation-tool/scripts
|
||||
./get-token.sh grabblaw.com investigator
|
||||
```
|
||||
|
||||
**Expected output:** A JWT token (long string starting with `eyJ...`)
|
||||
|
||||
**If it fails:**
|
||||
- Check vault_path in identity.json
|
||||
- Verify SOPS files exist
|
||||
- Verify age key is configured
|
||||
- Check file permissions
|
||||
|
||||
---
|
||||
|
||||
## Test Results (Attempted 2026-04-21)
|
||||
|
||||
**Clone attempt 1:**
|
||||
```
|
||||
git clone http://172.16.3.20:3000/azcomputerguru/vault.git ~/vault
|
||||
→ fatal: could not read Username for 'http://172.16.3.20:3000': Device not configured
|
||||
```
|
||||
|
||||
**Clone attempt 2:**
|
||||
```
|
||||
git clone http://azcomputerguru@172.16.3.20:3000/azcomputerguru/vault.git ~/vault
|
||||
→ fatal: could not read Password for 'http://azcomputerguru@172.16.3.20:3000': Device not configured
|
||||
```
|
||||
|
||||
**Blocker:** Git on Mac cannot prompt for credentials in this terminal session.
|
||||
|
||||
**Workaround needed:** Configure credential helper or use SSH authentication.
|
||||
|
||||
---
|
||||
|
||||
## Why This Matters
|
||||
|
||||
**Once vault is set up on Mac:**
|
||||
- Can test remediation-tool locally
|
||||
- Can run breach checks without switching to Windows
|
||||
- Full parity with Windows/Howard's machines
|
||||
- Validates that vault sync from Windows worked
|
||||
|
||||
**Current capability:**
|
||||
- remediation-tool scripts are executable ✓
|
||||
- get-token.sh bugs are fixed ✓
|
||||
- Vault wrapper logic is correct ✓
|
||||
- **Blocked only by vault clone authentication** ✗
|
||||
|
||||
---
|
||||
|
||||
## Alternative: Test on Windows
|
||||
|
||||
If Mac vault setup is low priority, the vault sync can be validated on Windows:
|
||||
|
||||
```bash
|
||||
cd D:\vault
|
||||
git pull origin main
|
||||
ls D:\vault\msp-tools\computerguru-*.sops.yaml
|
||||
|
||||
cd D:\ClaudeTools\.claude\skills\remediation-tool\scripts
|
||||
bash get-token.sh grabblaw.com investigator
|
||||
```
|
||||
|
||||
Should return a JWT token proving the vault sync worked.
|
||||
|
||||
---
|
||||
|
||||
**Next action:**
|
||||
- **If Mac needs remediation-tool:** Set up vault clone with proper authentication
|
||||
- **If Mac is just for testing:** Test vault sync on Windows instead
|
||||
- **If vault not needed on Mac:** Skip this setup entirely
|
||||
|
||||
**Priority:** LOW - Windows already has working vault + remediation-tool
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -1,14 +1,26 @@
|
||||
# Backups (local only - don't commit to repo)
|
||||
backups/
|
||||
|
||||
# Remediation-tool cache (live Graph API responses — may contain user data)
|
||||
.cache-remediation/
|
||||
tmp-remediation/
|
||||
|
||||
# Local settings (machine-specific)
|
||||
.claude/settings.local.json
|
||||
.claude/identity.json
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.log
|
||||
*.bak
|
||||
|
||||
# Live secrets / tokens — never commit
|
||||
.token
|
||||
.token_*
|
||||
*.jwt
|
||||
token.txt
|
||||
.token.txt
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "projects/msp-tools/guru-rmm"]
|
||||
path = projects/msp-tools/guru-rmm
|
||||
url = https://git.azcomputerguru.com/azcomputerguru/gururmm.git
|
||||
264
CONTEXT.md
Normal file
264
CONTEXT.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# ClaudeTools - Project Context
|
||||
|
||||
**Last Updated:** 2026-04-14
|
||||
**Status:** Active - Production Stable
|
||||
|
||||
## Quick Start - Infrastructure Overview
|
||||
|
||||
| Component | Location | Access | Notes |
|
||||
|-----------|----------|--------|-------|
|
||||
| **Production API** | http://172.16.3.30:8001 | Public access | ClaudeTools work tracking API |
|
||||
| **Production DB** | MariaDB @ 172.16.3.30:3306/claudetools | Vault credentials | 38 tables, AES-256-GCM encryption |
|
||||
| **Vault (SOPS)** | D:\vault\ | age-encrypted YAML | Primary credential store |
|
||||
| **1Password** | Service account | Fallback | op://Projects/ClaudeTools * |
|
||||
| **Gitea Repo** | git.azcomputerguru.com/azcomputerguru/claudetools | Active development | Main codebase |
|
||||
|
||||
**Get DB credentials:**
|
||||
```bash
|
||||
bash D:/vault/scripts/vault.sh get-field projects/claudetools/database.sops.yaml credentials.password
|
||||
```
|
||||
|
||||
## Current State (READ THIS FIRST)
|
||||
|
||||
### Project Status
|
||||
- **API:** 95+ endpoints, production-stable
|
||||
- **Database:** 38 tables, fully encrypted sensitive fields
|
||||
- **Authentication:** JWT-based, AES-256-GCM for API keys
|
||||
- **Deployment:** Auto-deploy via Gitea webhooks (planned)
|
||||
|
||||
### Active Subprojects
|
||||
1. **GuruRMM** - Remote monitoring system (see projects/msp-tools/guru-rmm/CONTEXT.md)
|
||||
2. **Dataforth DOS** - Test datasheet pipeline (see projects/dataforth-dos/CONTEXT.md)
|
||||
|
||||
### Session Logs
|
||||
- **Project-specific:** projects/*/session-logs/
|
||||
- **Client work:** clients/*/session-logs/
|
||||
- **General:** session-logs/ (root)
|
||||
|
||||
## Anti-Patterns (DON'T DO THIS)
|
||||
|
||||
❌ **DO NOT query database directly** - Use Database Agent for ALL operations
|
||||
|
||||
❌ **DO NOT write production code yourself** - Delegate to Coding Agent, coordinate as needed
|
||||
|
||||
❌ **DO NOT use emojis** - ASCII markers only: [OK], [ERROR], [WARNING], [SUCCESS], [INFO]
|
||||
|
||||
❌ **DO NOT hardcode credentials** - Always use SOPS vault (primary) or 1Password (fallback)
|
||||
|
||||
❌ **DO NOT skip Code Review Agent** - MANDATORY after any code changes
|
||||
|
||||
❌ **DO NOT execute >500 token operations directly** - Delegate to appropriate agent
|
||||
|
||||
## Where to Find Things
|
||||
|
||||
### Repository Structure
|
||||
```
|
||||
ClaudeTools/
|
||||
├── .claude/
|
||||
│ ├── CLAUDE.md # Project instructions (directives)
|
||||
│ ├── REFERENCE.md # Technical reference
|
||||
│ ├── CODING_GUIDELINES.md # Code standards
|
||||
│ ├── FILE_PLACEMENT_GUIDE.md # Where to save files
|
||||
│ ├── agents/ # Agent definitions
|
||||
│ └── memory/ # Persistent facts (syncs via Git)
|
||||
├── credentials.md # Infrastructure reference (migrating to vault)
|
||||
├── session-logs/ # General session logs
|
||||
├── projects/
|
||||
│ ├── msp-tools/guru-rmm/ # GuruRMM (CONTEXT.md there)
|
||||
│ ├── dataforth-dos/ # Dataforth (CONTEXT.md there)
|
||||
│ └── claudetools-api/ # API codebase (legacy)
|
||||
├── clients/
|
||||
│ └── [client-name]/ # Client-specific work
|
||||
└── D:\vault\ # SOPS encrypted credentials (separate repo)
|
||||
```
|
||||
|
||||
### Credential Locations
|
||||
|
||||
**Primary: SOPS Vault (D:\vault\)**
|
||||
```bash
|
||||
# Search for keywords (no decryption needed)
|
||||
bash D:/vault/scripts/vault.sh search "claudetools"
|
||||
|
||||
# Get specific field
|
||||
bash D:/vault/scripts/vault.sh get-field projects/claudetools/database.sops.yaml credentials.password
|
||||
|
||||
# List all entries
|
||||
bash D:/vault/scripts/vault.sh list
|
||||
```
|
||||
|
||||
**Structure:**
|
||||
- infrastructure/ - Servers, network gear
|
||||
- clients/ - Client-specific credentials
|
||||
- services/ - External services (GitHub, APIs)
|
||||
- projects/ - Project databases, APIs
|
||||
- msp-tools/ - MSP application credentials
|
||||
|
||||
**Fallback: 1Password**
|
||||
```bash
|
||||
# ClaudeTools database credentials
|
||||
op read "op://Projects/ClaudeTools Database/password"
|
||||
|
||||
# ClaudeTools API auth
|
||||
op read "op://Projects/ClaudeTools API Auth/credential"
|
||||
```
|
||||
|
||||
## Common Operations
|
||||
|
||||
### Start Work on Subproject
|
||||
```
|
||||
User: "Let's work on GuruRMM tunnel Phase 2"
|
||||
|
||||
Claude should:
|
||||
1. Read projects/msp-tools/guru-rmm/CONTEXT.md (this file)
|
||||
2. Check recent session logs referenced in CONTEXT.md
|
||||
3. Understand current state, infrastructure, anti-patterns
|
||||
4. Proceed without asking user for context
|
||||
|
||||
DO NOT:
|
||||
- Ask user "what's the server IP?"
|
||||
- Ask user "where is the database?"
|
||||
- Ask user "what credentials should I use?"
|
||||
All of this is in CONTEXT.md
|
||||
```
|
||||
|
||||
### Database Operations
|
||||
```bash
|
||||
# WRONG: Direct query
|
||||
ssh user@172.16.3.30 "mysql -u claudetools -p claudetools -e 'SELECT * FROM ...'"
|
||||
|
||||
# RIGHT: Delegate to Database Agent
|
||||
"Use Database Agent to query work_logs table for entries from last 7 days"
|
||||
```
|
||||
|
||||
### Deploy Code Changes
|
||||
```bash
|
||||
# WRONG: Deploy yourself
|
||||
scp file.js user@172.16.3.30:/path/
|
||||
|
||||
# RIGHT: Follow project deployment process
|
||||
# (See project-specific CONTEXT.md for deployment steps)
|
||||
```
|
||||
|
||||
## Project-Specific Context Files
|
||||
|
||||
**When user says "work on [project]", read that project's CONTEXT.md FIRST:**
|
||||
|
||||
| Project | CONTEXT.md Location | What It Contains |
|
||||
|---------|---------------------|------------------|
|
||||
| GuruRMM | projects/msp-tools/guru-rmm/CONTEXT.md | Server IPs, deployment, tunnel architecture, agent status |
|
||||
| Dataforth DOS | projects/dataforth-dos/CONTEXT.md | AD2/AD1 infrastructure, testdatadb service, log formats |
|
||||
| ClaudeTools API | (This file) | Main project overview, credential locations |
|
||||
|
||||
## Coordination Rules (My Role)
|
||||
|
||||
I am a **Coordinator**, NOT an executor:
|
||||
|
||||
| Operation | Delegate To |
|
||||
|-----------|-------------|
|
||||
| Database queries/updates | Database Agent |
|
||||
| Production code generation | Coding Agent |
|
||||
| Code review (MANDATORY) | Code Review Agent |
|
||||
| Test execution | Testing Agent |
|
||||
| Git commit/push | Gitea Agent |
|
||||
| Backups/restore | Backup Agent |
|
||||
| File exploration | Explore Agent |
|
||||
| Semantic code search | deep-explore Agent (GrepAI) |
|
||||
| Complex reasoning | General-purpose + Sequential Thinking |
|
||||
|
||||
**I do myself:** Simple responses, reading 1-2 files, presenting results, planning, decisions
|
||||
|
||||
**Rule:** >500 tokens of work = delegate
|
||||
|
||||
## Memory System
|
||||
|
||||
**Location:** `.claude/memory/` (syncs across machines via Git)
|
||||
|
||||
**Structure:**
|
||||
- MEMORY.md - Index of all facts
|
||||
- [topic]-context.md - Topic-specific persistent facts
|
||||
|
||||
**IMPORTANT:** Always write to `.claude/memory/` (repo-relative), NOT `~/.claude/projects/*/memory/`
|
||||
|
||||
## Session Log Locations
|
||||
|
||||
**Follow these rules:**
|
||||
|
||||
| Work Type | Save To |
|
||||
|-----------|---------|
|
||||
| Dataforth DOS work | projects/dataforth-dos/session-logs/ |
|
||||
| ClaudeTools API code | projects/claudetools-api/session-logs/ |
|
||||
| GuruRMM work | projects/msp-tools/guru-rmm/session-logs/ |
|
||||
| Client work | clients/[client-name]/session-logs/ |
|
||||
| General/mixed work | session-logs/ (root) |
|
||||
|
||||
**See:** .claude/FILE_PLACEMENT_GUIDE.md for full rules
|
||||
|
||||
## Auto-Invoke Skills
|
||||
|
||||
**Frontend Design:** Auto-invoke `/frontend-design` skill after ANY UI change (HTML/CSS/JSX/styling)
|
||||
|
||||
**Sequential Thinking:** Use for genuine complexity only:
|
||||
- Rejection loops (3+ failed attempts)
|
||||
- Critical architectural decisions
|
||||
- Multi-step debugging with unclear root cause
|
||||
- NOT for every task
|
||||
|
||||
## Key Commands
|
||||
|
||||
| Command | Purpose | When |
|
||||
|---------|---------|------|
|
||||
| `/checkpoint` | Git commit + database context save | After code changes |
|
||||
| `/save` | Comprehensive session log | End of session |
|
||||
| `/context` | Search session logs + credentials | User references previous work |
|
||||
| `/1password` | 1Password operations | Manage secrets |
|
||||
| `/sync` | Sync from Gitea | Update local config |
|
||||
| `/refresh-directives` | Re-read CLAUDE.md | After summarization, large tasks |
|
||||
|
||||
## Reference Documents (Read On-Demand)
|
||||
|
||||
- **Project instructions:** .claude/CLAUDE.md (my role, agent delegation rules)
|
||||
- **Technical reference:** .claude/REFERENCE.md (endpoints, database, workflows)
|
||||
- **Coding standards:** .claude/CODING_GUIDELINES.md (agents read, not every session)
|
||||
- **File placement:** .claude/FILE_PLACEMENT_GUIDE.md (where to save files)
|
||||
- **MCP servers:** MCP_SERVERS.md (available integrations)
|
||||
- **Agent definitions:** .claude/agents/*.md (agent capabilities)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "I don't know the database password"
|
||||
- **Check:** D:\vault\ (SOPS encrypted)
|
||||
- **Command:** `bash D:/vault/scripts/vault.sh get-field projects/claudetools/database.sops.yaml credentials.password`
|
||||
- **Fallback:** credentials.md (has 1Password references)
|
||||
|
||||
### "Where is the GuruRMM server?"
|
||||
- **Check:** projects/msp-tools/guru-rmm/CONTEXT.md
|
||||
- **Answer:** 172.16.3.30 (listed in first table)
|
||||
|
||||
### "How do I deploy Dataforth changes?"
|
||||
- **Check:** projects/dataforth-dos/CONTEXT.md
|
||||
- **Section:** "Common Operations → Deploy Code to AD2"
|
||||
|
||||
### "I forgot my role as Coordinator"
|
||||
- **Read:** .claude/CLAUDE.md
|
||||
- **Command:** `/refresh-directives`
|
||||
|
||||
## Quick Links
|
||||
|
||||
- **Credentials (vault):** D:\vault\ (SOPS encrypted YAML)
|
||||
- **Credentials (legacy):** credentials.md (migrating to vault)
|
||||
- **Gitea:** http://172.16.3.20:3000/azcomputerguru/claudetools
|
||||
- **API Docs:** http://172.16.3.30:8001/api/docs
|
||||
|
||||
---
|
||||
|
||||
**When user says "work on [project]":**
|
||||
1. Read [project]/CONTEXT.md FIRST (don't ask user for context)
|
||||
2. Check recent session logs mentioned in CONTEXT.md
|
||||
3. Understand infrastructure, anti-patterns, current state
|
||||
4. Proceed with work
|
||||
|
||||
**This eliminates:**
|
||||
- "What's the server IP?" → In CONTEXT.md
|
||||
- "Where's the database?" → In CONTEXT.md
|
||||
- "What credentials?" → In CONTEXT.md
|
||||
- "What did we do last time?" → In session logs referenced by CONTEXT.md
|
||||
222
README.md
Normal file
222
README.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# ClaudeTools Bootstrap / Reinstall Guide
|
||||
|
||||
Complete instructions for backing up and restoring a ClaudeTools development environment on Windows 11.
|
||||
|
||||
---
|
||||
|
||||
## Pre-Reinstall: Creating the Archive
|
||||
|
||||
Before wiping or reinstalling Windows, create a backup archive.
|
||||
|
||||
### Option A: Automated Archive (Recommended)
|
||||
|
||||
Run the bootstrap script in archive mode:
|
||||
|
||||
```powershell
|
||||
cd D:\ClaudeTools\bootstrap
|
||||
.\bootstrap.ps1 -Archive
|
||||
```
|
||||
|
||||
This creates `D:\ClaudeTools-backup.zip` containing:
|
||||
- The full ClaudeTools repository (excluding `node_modules`, `__pycache__`, `venv`)
|
||||
- Claude configuration and memory from `C:\Users\<you>\.claude\`
|
||||
|
||||
To specify a custom output path:
|
||||
|
||||
```powershell
|
||||
.\bootstrap.ps1 -Archive -ArchivePath "E:\Backups\claudetools-2026-03-17.zip"
|
||||
```
|
||||
|
||||
### Option B: Manual Archive
|
||||
|
||||
If the script is unavailable, manually zip these locations:
|
||||
|
||||
1. **ClaudeTools repository**: `D:\ClaudeTools\` (entire directory)
|
||||
2. **Claude memory and config**: `C:\Users\<you>\.claude\` (entire directory)
|
||||
|
||||
Copy the archive(s) to external storage (USB, NAS, cloud) before proceeding with the Windows reinstall.
|
||||
|
||||
### What Does NOT Need Archiving
|
||||
|
||||
These are restored automatically by the bootstrap script:
|
||||
- Git, Node.js, Python, Ollama (reinstalled via winget)
|
||||
- npm global packages (reinstalled)
|
||||
- Python pip packages (reinstalled)
|
||||
- Ollama models (re-pulled)
|
||||
- MCP server virtual environments (recreated)
|
||||
|
||||
---
|
||||
|
||||
## Post-Reinstall: Running the Bootstrap
|
||||
|
||||
### Step 1: Prepare the D: Drive
|
||||
|
||||
If `D:\ClaudeTools` was on a separate partition that survived the reinstall, skip to Step 2.
|
||||
|
||||
Otherwise, extract your archive:
|
||||
|
||||
```powershell
|
||||
# Extract the ClaudeTools repo to D:\
|
||||
Expand-Archive -Path "E:\Backups\claudetools-2026-03-17.zip" -DestinationPath "D:\"
|
||||
|
||||
# Extract Claude config to your user profile
|
||||
# (The archive contains a 'claude-config' folder - copy it to the right place)
|
||||
Copy-Item -Path "D:\claude-config\*" -Destination "$env:USERPROFILE\.claude\" -Recurse -Force
|
||||
```
|
||||
|
||||
### Step 2: Run the Bootstrap Script
|
||||
|
||||
Open an **elevated PowerShell** (Run as Administrator):
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||
D:\ClaudeTools\bootstrap\bootstrap.ps1
|
||||
```
|
||||
|
||||
The script runs 9 phases and takes approximately 15-30 minutes depending on download speeds and Ollama model sizes.
|
||||
|
||||
### Step 3: Advanced Usage
|
||||
|
||||
Run a single phase:
|
||||
|
||||
```powershell
|
||||
.\bootstrap.ps1 -OnlyPhase 4 # Only install Python packages
|
||||
```
|
||||
|
||||
Skip specific phases:
|
||||
|
||||
```powershell
|
||||
.\bootstrap.ps1 -SkipPhase 5 # Skip Ollama model pulls (slow)
|
||||
.\bootstrap.ps1 -SkipPhase 4,5 # Skip Python packages and Ollama models
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase Reference
|
||||
|
||||
| Phase | What It Does | Duration |
|
||||
|-------|-------------|----------|
|
||||
| 1 | Install Git, Node.js, Python 3.13, Ollama via winget | 2-5 min |
|
||||
| 2 | Install Claude Code CLI + global npm packages | 1-2 min |
|
||||
| 3 | Clone or configure ClaudeTools git repository | <1 min |
|
||||
| 4 | Install all Python pip packages globally | 3-5 min |
|
||||
| 5 | Pull Ollama models (nomic-embed-text, llama3.1:8b, qwen2.5-coder:7b) | 5-15 min |
|
||||
| 6 | Create MCP server venv and install dependencies | 1-2 min |
|
||||
| 7 | Write Claude Code settings.json, copy commands, create directories | <1 min |
|
||||
| 8 | Initialize GrepAI | <1 min |
|
||||
| 9 | Verify all components are installed and working | <1 min |
|
||||
|
||||
---
|
||||
|
||||
## Manual Steps (Cannot Be Automated)
|
||||
|
||||
These steps require interactive authentication or browser actions:
|
||||
|
||||
### 1. Authenticate Claude Code
|
||||
|
||||
```powershell
|
||||
claude
|
||||
```
|
||||
|
||||
Follow the prompts to enter your Anthropic API key or log in via browser.
|
||||
|
||||
### 2. GitHub Personal Access Token
|
||||
|
||||
Edit `D:\ClaudeTools\.mcp.json` and set the `GITHUB_PERSONAL_ACCESS_TOKEN` value:
|
||||
|
||||
```json
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token_here"
|
||||
}
|
||||
```
|
||||
|
||||
Generate a new token at: https://github.com/settings/tokens
|
||||
|
||||
### 3. Claude-in-Chrome Extension
|
||||
|
||||
Install the Chrome extension manually:
|
||||
- Open Chrome and navigate to the Chrome Web Store
|
||||
- Search for "Claude in Chrome" (or install from the MCP extension source)
|
||||
- Configure the extension to connect to your local MCP server
|
||||
|
||||
### 4. Restore Memory Files (If Needed)
|
||||
|
||||
If the bootstrap reports memory files are missing:
|
||||
|
||||
```powershell
|
||||
# Copy from your archive
|
||||
Copy-Item -Path "E:\Backups\claude-config\projects\D--ClaudeTools\memory\*" `
|
||||
-Destination "$env:USERPROFILE\.claude\projects\D--ClaudeTools\memory\" `
|
||||
-Recurse -Force
|
||||
```
|
||||
|
||||
### 5. Git Credentials
|
||||
|
||||
When you first `git pull` or `git push` to Gitea, you will be prompted for credentials. Use the Gitea username and password from `credentials.md`.
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After bootstrap completes, verify manually:
|
||||
|
||||
- [ ] `git --version` returns a version
|
||||
- [ ] `node --version` returns v24.x or later
|
||||
- [ ] `python --version` returns 3.13.x
|
||||
- [ ] `claude --version` returns a version
|
||||
- [ ] `ollama list` shows all 3 models
|
||||
- [ ] `D:\ClaudeTools` exists and has `.git` directory
|
||||
- [ ] `D:\ClaudeTools\.mcp.json` exists
|
||||
- [ ] `D:\ClaudeTools\grepai.exe` exists
|
||||
- [ ] `C:\Users\<you>\.claude\settings.json` exists
|
||||
- [ ] `C:\Users\<you>\.claude\commands\` has command files
|
||||
- [ ] Run `claude` from `D:\ClaudeTools` and confirm MCP servers connect
|
||||
- [ ] Claude-in-Chrome extension is installed and responsive
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### winget not found
|
||||
Install "App Installer" from the Microsoft Store. It ships with Windows 11 but may need updating.
|
||||
|
||||
### Node.js/Python not on PATH after install
|
||||
Close and reopen your terminal, or run:
|
||||
```powershell
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
|
||||
```
|
||||
|
||||
### Ollama models fail to pull
|
||||
Ensure the Ollama service is running:
|
||||
```powershell
|
||||
ollama serve
|
||||
```
|
||||
Then retry:
|
||||
```powershell
|
||||
.\bootstrap.ps1 -OnlyPhase 5
|
||||
```
|
||||
|
||||
### pip install fails for specific packages
|
||||
Some packages (pywin32, opencv-python, pyzbar) require Visual C++ Build Tools. Install if needed:
|
||||
```powershell
|
||||
winget install Microsoft.VisualStudio.2022.BuildTools
|
||||
```
|
||||
Then re-run Phase 4:
|
||||
```powershell
|
||||
.\bootstrap.ps1 -OnlyPhase 4
|
||||
```
|
||||
|
||||
### GrepAI init requires interaction
|
||||
Run manually:
|
||||
```powershell
|
||||
cd D:\ClaudeTools
|
||||
.\grepai.exe init
|
||||
```
|
||||
Select Ollama as the provider and nomic-embed-text as the embedding model.
|
||||
|
||||
### SSL certificate errors with Gitea
|
||||
The bootstrap configures `http.sslVerify false` automatically. If you still see errors:
|
||||
```powershell
|
||||
cd D:\ClaudeTools
|
||||
git config http.sslVerify false
|
||||
```
|
||||
46
WORKITEMS.md
Normal file
46
WORKITEMS.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Shared Work Items
|
||||
|
||||
Tag yourself to claim. Check off when done. Add new items at the bottom of the relevant section.
|
||||
|
||||
**Syntax:** `- [ ] Description — @mike/@howard/@unassigned | added YYYY-MM-DD`
|
||||
|
||||
---
|
||||
|
||||
## Active
|
||||
|
||||
- [ ] Deploy session manager to SAGE-SQL (IIS app, Windows Auth) — files ready at `clients/dataforth/session-manager/` — @mike | added 2026-04-17
|
||||
- [x] Cascades Synology (cascadesds) — get admin creds, add to vault — @howard | done 2026-04-17 (vault: `clients/cascades-tucson/synology-cascadesds.sops.yaml`)
|
||||
- [ ] Cascades — second Life Enrichment machine: end-to-end folder redirection test (tomorrow). See `clients/cascades-tucson/session-logs/2026-04-17-howard-cascades-onboarding-and-folder-redirection.md` — @howard | added 2026-04-17
|
||||
- [ ] Cascades GPO — add Desktop/Pictures/Music/Videos/Favorites once 2nd machine validates the pattern, and retire the DLTAGOI Desktop reg hack — @howard | added 2026-04-17
|
||||
- [ ] Cascades — build matching folder-redirection GPOs for every other department (Nursing, Admin, Maintenance, etc.) once Life Enrichment is proven — @howard | added 2026-04-17
|
||||
- [ ] Cascades — design OneDrive-to-server migration plan (machines with Documents/Desktop already in OneDrive KFM need data-migration + unlink BEFORE the GPO applies) — @unassigned | added 2026-04-17
|
||||
- [ ] Cascades HIPAA hardening — `Set-SmbShare -Name homes -EncryptData $true`, enable file-access auditing on D:\Homes, verify BitLocker on CS-SERVER D: — @unassigned | added 2026-04-17
|
||||
- [ ] GuruRMM bug — agent command executor can wedge after a user-context PS command hangs; doesn't recover on reboot. File + fix. — @mike | added 2026-04-17
|
||||
- [ ] Howard Gitea account — create via web UI at git.azcomputerguru.com — @mike | added 2026-04-16
|
||||
- [ ] desertrat.com — add DMARC p=reject + harden SPF on Route 53 (need AWS access) — @unassigned | added 2026-04-17
|
||||
- [ ] desertrat.com — long-term migration from WebSvr to IX + MailProtector — @unassigned | added 2026-04-17
|
||||
- [ ] MVAN other domains — only mvaninc.com has DMARC; client has other domains needing protection — @unassigned | added 2026-04-17
|
||||
- [ ] Glaztech Syncro ticket #32165 — timer entry billed wrong (should be comment+time); fix in Syncro GUI — @mike | added 2026-04-17
|
||||
- [ ] jparkinsonaz.com certbot — retry autodiscover cert once A record TTL expires — @unassigned | added 2026-04-17
|
||||
- [ ] Neptune jparkinson password — set to jP$48504850, verify mail working — @unassigned | added 2026-04-17
|
||||
- [ ] Len's Auto Brokerage — deploy GuruRMM v0.6.1 to 10 Windows endpoints — @mike | added 2026-04-16
|
||||
- [ ] GuruRMM server migration 5 — sqlx checksum drift blocks new server build — @mike | added 2026-04-16
|
||||
- [ ] Jupiter Windows VM — Server 2022 build worker for MSI CI — @unassigned | added 2026-04-16
|
||||
- [ ] Cloudflare SXG — disable via dashboard (API tokens lack scope), auto-removes June 23 — @unassigned | added 2026-04-17
|
||||
- [ ] GrepAI index — run `grepai watch` to build semantic search index — @unassigned | added 2026-04-16
|
||||
- [ ] Change LAN subnet for ACG-DC16/NEPTUNE on Dataforth network — current 172.16.x.x collides with ACG network (172.16.x.x/22) — @unassigned | added 2026-04-18
|
||||
- [ ] Remediation-tool vault gap — 5 tiered Entra apps (investigator, exchange-operator, user-manager, tenant-admin, defender-addon) are referenced by the `remediation-tool` skill but none of the SOPS files exist at `D:/vault/msp-tools/computerguru-*.sops.yaml`. Currently falling back to legacy `claude-msp-access-graph-api` app (broad Graph RW scope). Need Mike to: (1) confirm whether the 5 apps are already registered in Entra — if yes, hand over client IDs + secrets for the vault; (2) if not registered, decide: create the tiered apps or stay on legacy app. Impact: least-privilege model not enforced, bigger blast radius on the one shared secret, and Defender-tier checks unavailable until the MDE add-on app exists. Today's Cascades license audit succeeded on the fallback path — no action required from Howard yet. — @mike | added 2026-04-21
|
||||
|
||||
## Completed
|
||||
|
||||
_Move items here when done. Keep for 30 days then delete._
|
||||
|
||||
---
|
||||
|
||||
## How to use
|
||||
|
||||
- **Claim:** change `@unassigned` to your name
|
||||
- **Add:** append to Active section with today's date
|
||||
- **Complete:** move to Completed with date: `- [x] Description — @mike | done 2026-04-18`
|
||||
- **Claude:** say "show work items" or "add work item: ..." and Claude reads/updates this file
|
||||
- **Sync:** items sync via `/sync` like everything else
|
||||
@@ -53,6 +53,10 @@ class Settings(BaseSettings):
|
||||
GRAPH_SENDER_EMAIL: str = "noreply@azcomputerguru.com"
|
||||
ADMIN_NOTIFICATION_EMAIL: str = "mike@azcomputerguru.com"
|
||||
|
||||
# Bitdefender GravityZone
|
||||
GRAVITYZONE_API_KEY: str = ""
|
||||
GRAVITYZONE_API_BASE_URL: str = "https://cloud.gravityzone.bitdefender.com/api/v1.0/jsonrpc"
|
||||
|
||||
class Config:
|
||||
"""Pydantic configuration."""
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ from api.routers import (
|
||||
quotes,
|
||||
admin_quotes,
|
||||
ticktick,
|
||||
gravityzone,
|
||||
)
|
||||
|
||||
# Import middleware
|
||||
@@ -133,6 +134,7 @@ app.include_router(admin_quotes.router, prefix="/api/admin/quotes", tags=["Admin
|
||||
|
||||
# External integrations
|
||||
app.include_router(ticktick.router, prefix="/api/ticktick", tags=["TickTick"])
|
||||
app.include_router(gravityzone.router, prefix="/api/gravityzone", tags=["GravityZone"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
253
api/routers/gravityzone.py
Normal file
253
api/routers/gravityzone.py
Normal file
@@ -0,0 +1,253 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
|
||||
from api.middleware.auth import get_current_user
|
||||
from api.schemas.gravityzone import (
|
||||
GZCompanyItem,
|
||||
GZEndpointDetail,
|
||||
GZEndpointItem,
|
||||
GZStatusResponse,
|
||||
GZSweepResult,
|
||||
)
|
||||
from api.services.gravityzone_service import (
|
||||
ACG_COMPANIES_CONTAINER_ID,
|
||||
get_gravityzone_service,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _raise_on_failure(result, detail_prefix: str = "GravityZone error") -> dict:
|
||||
if not result.success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f"{detail_prefix}: {result.error or 'unknown error'}",
|
||||
)
|
||||
return result.data or {}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Status
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get(
|
||||
"/status",
|
||||
response_model=GZStatusResponse,
|
||||
summary="GravityZone API key status and license info",
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
async def get_status(current_user: dict = Depends(get_current_user)):
|
||||
service = get_gravityzone_service()
|
||||
result = await service.get_api_status()
|
||||
data = _raise_on_failure(result, "GravityZone status")
|
||||
|
||||
return GZStatusResponse(
|
||||
enabled_apis=data.get("enabledApis", []),
|
||||
key_created_at=data.get("createdAt"),
|
||||
used_slots=data.get("usedSlots"),
|
||||
total_slots=data.get("totalSlots"),
|
||||
expiry_date=data.get("expiryDate"),
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Companies
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get(
|
||||
"/companies",
|
||||
summary="List GravityZone client companies",
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
async def list_companies(
|
||||
page: int = Query(1, ge=1),
|
||||
per_page: int = Query(100, ge=1, le=500),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
service = get_gravityzone_service()
|
||||
result = await service.list_client_companies(page=page, per_page=per_page)
|
||||
data = _raise_on_failure(result, "GravityZone companies")
|
||||
|
||||
companies = [
|
||||
GZCompanyItem(
|
||||
id=item.get("id", ""),
|
||||
name=item.get("name", ""),
|
||||
type=item.get("type", 1),
|
||||
)
|
||||
for item in data.get("items", [])
|
||||
]
|
||||
return {"total": data.get("total", len(companies)), "companies": companies}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Endpoints
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get(
|
||||
"/companies/{company_id}/endpoints",
|
||||
summary="List endpoints for a GravityZone company",
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
async def list_endpoints(
|
||||
company_id: str,
|
||||
page: int = Query(1, ge=1),
|
||||
per_page: int = Query(50, ge=1, le=200),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
service = get_gravityzone_service()
|
||||
result = await service.list_endpoints(company_id, page=page, per_page=per_page)
|
||||
data = _raise_on_failure(result, "GravityZone endpoints")
|
||||
|
||||
endpoints = [
|
||||
GZEndpointItem(
|
||||
id=item.get("id", ""),
|
||||
name=item.get("name", ""),
|
||||
fqdn=item.get("fqdn"),
|
||||
ip=item.get("ip"),
|
||||
os_version=item.get("operatingSystemVersion"),
|
||||
is_managed=bool(item.get("isManaged", False)),
|
||||
policy_name=(item.get("policy") or {}).get("name"),
|
||||
)
|
||||
for item in data.get("items", [])
|
||||
]
|
||||
return {"total": data.get("total", len(endpoints)), "endpoints": endpoints}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/endpoints/{endpoint_id}",
|
||||
response_model=GZEndpointDetail,
|
||||
summary="Get detailed info for a single GravityZone endpoint",
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
async def get_endpoint(
|
||||
endpoint_id: str,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
service = get_gravityzone_service()
|
||||
result = await service.get_endpoint_details(endpoint_id)
|
||||
data = _raise_on_failure(result, "GravityZone endpoint detail")
|
||||
|
||||
malware = data.get("malwareStatus", {})
|
||||
agent = data.get("agent", {})
|
||||
|
||||
return GZEndpointDetail(
|
||||
id=data.get("id", endpoint_id),
|
||||
name=data.get("name", ""),
|
||||
company_id=data.get("companyId"),
|
||||
infected=bool(malware.get("infected", False)),
|
||||
detection_active=bool(malware.get("detection", False)),
|
||||
signature_outdated=bool(agent.get("signatureOutdated", False)),
|
||||
product_outdated=bool(agent.get("productOutdated", False)),
|
||||
agent_version=agent.get("productVersion"),
|
||||
engine_version=agent.get("engineVersion"),
|
||||
last_seen=data.get("lastSeen"),
|
||||
last_update=agent.get("lastUpdate"),
|
||||
state=data.get("state", 0),
|
||||
modules=data.get("modules"),
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Quarantine
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get(
|
||||
"/companies/{company_id}/quarantine",
|
||||
summary="List quarantine items for a GravityZone company",
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
async def list_quarantine(
|
||||
company_id: str,
|
||||
page: int = Query(1, ge=1),
|
||||
per_page: int = Query(50, ge=1, le=200),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
service = get_gravityzone_service()
|
||||
result = await service.list_quarantine_items(company_id, page=page, per_page=per_page)
|
||||
data = _raise_on_failure(result, "GravityZone quarantine")
|
||||
|
||||
return {"total": data.get("total", 0), "items": data.get("items", [])}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Security sweep
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _build_sweep_result(summaries) -> GZSweepResult:
|
||||
stale_cutoff = datetime.now(timezone.utc) - timedelta(days=7)
|
||||
not_seen_recently = 0
|
||||
|
||||
for s in summaries:
|
||||
if s.last_seen:
|
||||
try:
|
||||
last_seen_dt = datetime.fromisoformat(
|
||||
s.last_seen.replace("Z", "+00:00")
|
||||
)
|
||||
if last_seen_dt.tzinfo is None:
|
||||
last_seen_dt = last_seen_dt.replace(tzinfo=timezone.utc)
|
||||
if last_seen_dt < stale_cutoff:
|
||||
not_seen_recently += 1
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
|
||||
return GZSweepResult(
|
||||
total=len(summaries),
|
||||
infected=sum(1 for s in summaries if s.infected),
|
||||
signature_outdated=sum(1 for s in summaries if s.signature_outdated),
|
||||
product_outdated=sum(1 for s in summaries if s.product_outdated),
|
||||
not_seen_recently=not_seen_recently,
|
||||
endpoints=[
|
||||
{
|
||||
"endpoint_id": s.endpoint_id,
|
||||
"name": s.name,
|
||||
"company_id": s.company_id,
|
||||
"infected": s.infected,
|
||||
"detection_active": s.detection_active,
|
||||
"signature_outdated": s.signature_outdated,
|
||||
"product_outdated": s.product_outdated,
|
||||
"last_seen": s.last_seen,
|
||||
"agent_version": s.agent_version,
|
||||
"state": s.state,
|
||||
}
|
||||
for s in summaries
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/sweep/{parent_id}",
|
||||
response_model=GZSweepResult,
|
||||
summary="Security sweep for all endpoints under a parent ID",
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
async def sweep_parent(
|
||||
parent_id: str,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
service = get_gravityzone_service()
|
||||
summaries = await service.security_sweep(parent_id)
|
||||
return _build_sweep_result(summaries)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/sweep",
|
||||
response_model=GZSweepResult,
|
||||
summary="Security sweep across all ACG client companies",
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
async def sweep_all_clients(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
service = get_gravityzone_service()
|
||||
summaries = await service.security_sweep_all_clients()
|
||||
return _build_sweep_result(summaries)
|
||||
52
api/schemas/gravityzone.py
Normal file
52
api/schemas/gravityzone.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class GZEndpointItem(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
fqdn: Optional[str] = None
|
||||
ip: Optional[str] = None
|
||||
os_version: Optional[str] = None
|
||||
is_managed: bool
|
||||
policy_name: Optional[str] = None
|
||||
|
||||
|
||||
class GZEndpointDetail(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
company_id: Optional[str] = None
|
||||
infected: bool
|
||||
detection_active: bool
|
||||
signature_outdated: bool
|
||||
product_outdated: bool
|
||||
agent_version: Optional[str] = None
|
||||
engine_version: Optional[str] = None
|
||||
last_seen: Optional[str] = None
|
||||
last_update: Optional[str] = None
|
||||
state: int
|
||||
modules: Optional[dict] = None
|
||||
|
||||
|
||||
class GZCompanyItem(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
type: int
|
||||
|
||||
|
||||
class GZSweepResult(BaseModel):
|
||||
total: int
|
||||
infected: int
|
||||
signature_outdated: int
|
||||
product_outdated: int
|
||||
not_seen_recently: int
|
||||
endpoints: list[dict]
|
||||
|
||||
|
||||
class GZStatusResponse(BaseModel):
|
||||
enabled_apis: list[str]
|
||||
key_created_at: Optional[str] = None
|
||||
used_slots: Optional[int] = None
|
||||
total_slots: Optional[int] = None
|
||||
expiry_date: Optional[str] = None
|
||||
263
api/services/gravityzone_service.py
Normal file
263
api/services/gravityzone_service.py
Normal file
@@ -0,0 +1,263 @@
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
GRAVITYZONE_API_BASE_URL = os.environ.get(
|
||||
"GRAVITYZONE_API_BASE_URL",
|
||||
"https://cloud.gravityzone.bitdefender.com/api/v1.0/jsonrpc",
|
||||
)
|
||||
GRAVITYZONE_API_KEY = os.environ.get("GRAVITYZONE_API_KEY", "")
|
||||
|
||||
GRAVITYZONE_TIMEOUT_SECONDS = 30.0
|
||||
GRAVITYZONE_CONNECT_TIMEOUT_SECONDS = 10.0
|
||||
|
||||
ACG_ROOT_COMPANY_ID = "5c4280716c0318f3478b456a"
|
||||
ACG_COMPANIES_CONTAINER_ID = "5c4280716c0318f3478b456e"
|
||||
|
||||
|
||||
@dataclass
|
||||
class GZResult:
|
||||
success: bool
|
||||
data: Optional[dict] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class GZEndpointSummary:
|
||||
endpoint_id: str
|
||||
name: str
|
||||
company_id: str
|
||||
infected: bool
|
||||
detection_active: bool
|
||||
signature_outdated: bool
|
||||
product_outdated: bool
|
||||
last_seen: Optional[str]
|
||||
agent_version: Optional[str]
|
||||
state: int
|
||||
|
||||
|
||||
class GravityZoneService:
|
||||
def __init__(
|
||||
self,
|
||||
api_base_url: str = GRAVITYZONE_API_BASE_URL,
|
||||
api_key: str = GRAVITYZONE_API_KEY,
|
||||
timeout: float = GRAVITYZONE_TIMEOUT_SECONDS,
|
||||
connect_timeout: float = GRAVITYZONE_CONNECT_TIMEOUT_SECONDS,
|
||||
):
|
||||
self.api_base_url = api_base_url.rstrip("/")
|
||||
self.api_key = api_key
|
||||
self.timeout = httpx.Timeout(timeout, connect=connect_timeout)
|
||||
|
||||
async def _jsonrpc_request(
|
||||
self, module: str, method: str, params: dict
|
||||
) -> GZResult:
|
||||
url = f"{self.api_base_url}/{module}"
|
||||
payload = {"id": "1", "jsonrpc": "2.0", "method": method, "params": params}
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.post(url, json=payload, auth=(self.api_key, ""))
|
||||
response.raise_for_status()
|
||||
body = response.json()
|
||||
except httpx.TimeoutException as exc:
|
||||
return GZResult(success=False, error=f"GravityZone API timeout: {exc}")
|
||||
except httpx.HTTPStatusError as exc:
|
||||
return GZResult(
|
||||
success=False,
|
||||
error=f"GravityZone HTTP error {exc.response.status_code}: {exc.response.text}",
|
||||
)
|
||||
except Exception as exc:
|
||||
return GZResult(success=False, error=f"GravityZone request failed: {exc}")
|
||||
|
||||
if "error" in body:
|
||||
err = body["error"]
|
||||
detail = err.get("data", {}).get("details") or err.get("message")
|
||||
return GZResult(success=False, error=detail)
|
||||
return GZResult(success=True, data=body.get("result"))
|
||||
|
||||
async def get_api_status(self) -> GZResult:
|
||||
key_result = await self._jsonrpc_request("general", "getApiKeyDetails", {})
|
||||
license_result = await self._jsonrpc_request("licensing", "getLicenseInfo", {})
|
||||
|
||||
if not key_result.success:
|
||||
return key_result
|
||||
|
||||
combined = {**(key_result.data or {})}
|
||||
if license_result.success:
|
||||
combined.update(license_result.data or {})
|
||||
else:
|
||||
logger.warning(f"GravityZone getLicenseInfo failed: {license_result.error}")
|
||||
|
||||
return GZResult(success=True, data=combined)
|
||||
|
||||
async def get_own_company(self) -> GZResult:
|
||||
return await self._jsonrpc_request("companies", "getCompanyDetails", {})
|
||||
|
||||
async def list_client_companies(self, page: int = 1, per_page: int = 100) -> GZResult:
|
||||
result = await self._jsonrpc_request(
|
||||
"network",
|
||||
"getNetworkInventoryItems",
|
||||
{
|
||||
"parentId": ACG_COMPANIES_CONTAINER_ID,
|
||||
"page": page,
|
||||
"perPage": per_page,
|
||||
},
|
||||
)
|
||||
if not result.success:
|
||||
return result
|
||||
|
||||
data = result.data or {}
|
||||
items = data.get("items", [])
|
||||
companies = [item for item in items if item.get("type") == 1]
|
||||
return GZResult(
|
||||
success=True,
|
||||
data={"total": len(companies), "items": companies},
|
||||
)
|
||||
|
||||
async def list_endpoints(
|
||||
self, parent_id: str, page: int = 1, per_page: int = 50
|
||||
) -> GZResult:
|
||||
return await self._jsonrpc_request(
|
||||
"network",
|
||||
"getEndpointsList",
|
||||
{"parentId": parent_id, "page": page, "perPage": per_page},
|
||||
)
|
||||
|
||||
async def get_endpoint_details(self, endpoint_id: str) -> GZResult:
|
||||
return await self._jsonrpc_request(
|
||||
"network",
|
||||
"getManagedEndpointDetails",
|
||||
{"endpointId": endpoint_id},
|
||||
)
|
||||
|
||||
async def list_quarantine_items(
|
||||
self, parent_id: str, page: int = 1, per_page: int = 50
|
||||
) -> GZResult:
|
||||
return await self._jsonrpc_request(
|
||||
"quarantine",
|
||||
"getQuarantineItemsList",
|
||||
{"parentId": parent_id, "page": page, "perPage": per_page},
|
||||
)
|
||||
|
||||
async def security_sweep(self, parent_id: str) -> list[GZEndpointSummary]:
|
||||
summaries: list[GZEndpointSummary] = []
|
||||
page = 1
|
||||
per_page = 100
|
||||
|
||||
while True:
|
||||
result = await self.list_endpoints(parent_id, page=page, per_page=per_page)
|
||||
if not result.success:
|
||||
logger.warning(
|
||||
f"GravityZone security_sweep list_endpoints failed for "
|
||||
f"{parent_id} page {page}: {result.error}"
|
||||
)
|
||||
break
|
||||
|
||||
data = result.data or {}
|
||||
items = data.get("items", [])
|
||||
if not items:
|
||||
break
|
||||
|
||||
for item in items:
|
||||
endpoint_id = item.get("id", "")
|
||||
detail_result = await self.get_endpoint_details(endpoint_id)
|
||||
if not detail_result.success:
|
||||
logger.warning(
|
||||
f"GravityZone getManagedEndpointDetails failed for "
|
||||
f"{endpoint_id}: {detail_result.error}"
|
||||
)
|
||||
continue
|
||||
|
||||
detail = detail_result.data or {}
|
||||
malware = detail.get("malwareStatus", {})
|
||||
agent = detail.get("agent", {})
|
||||
|
||||
infected = bool(malware.get("infected", False))
|
||||
detection_active = bool(malware.get("detection", False))
|
||||
signature_outdated = bool(agent.get("signatureOutdated", False))
|
||||
product_outdated = bool(agent.get("productOutdated", False))
|
||||
|
||||
if infected:
|
||||
logger.warning(
|
||||
f"GravityZone: infected endpoint detected — "
|
||||
f"id={endpoint_id} name={detail.get('name', '')}"
|
||||
)
|
||||
|
||||
summaries.append(
|
||||
GZEndpointSummary(
|
||||
endpoint_id=endpoint_id,
|
||||
name=detail.get("name") or item.get("name", ""),
|
||||
company_id=item.get("companyId", ""),
|
||||
infected=infected,
|
||||
detection_active=detection_active,
|
||||
signature_outdated=signature_outdated,
|
||||
product_outdated=product_outdated,
|
||||
last_seen=detail.get("lastSeen"),
|
||||
agent_version=agent.get("productVersion"),
|
||||
state=detail.get("state", 0),
|
||||
)
|
||||
)
|
||||
|
||||
total = data.get("total", 0)
|
||||
if page * per_page >= total:
|
||||
break
|
||||
page += 1
|
||||
|
||||
def _sort_key(s: GZEndpointSummary) -> tuple:
|
||||
return (
|
||||
not s.infected,
|
||||
not s.signature_outdated,
|
||||
not s.product_outdated,
|
||||
s.name.lower(),
|
||||
)
|
||||
|
||||
summaries.sort(key=_sort_key)
|
||||
return summaries
|
||||
|
||||
async def security_sweep_all_clients(self) -> list[GZEndpointSummary]:
|
||||
companies_result = await self.list_client_companies(per_page=100)
|
||||
if not companies_result.success:
|
||||
logger.warning(
|
||||
f"GravityZone security_sweep_all_clients: list_client_companies failed: "
|
||||
f"{companies_result.error}"
|
||||
)
|
||||
return []
|
||||
|
||||
companies = (companies_result.data or {}).get("items", [])
|
||||
all_summaries: list[GZEndpointSummary] = []
|
||||
|
||||
for company in companies:
|
||||
company_id = company.get("id", "")
|
||||
if not company_id:
|
||||
continue
|
||||
company_summaries = await self.security_sweep(company_id)
|
||||
for s in company_summaries:
|
||||
if not s.company_id:
|
||||
s.company_id = company_id
|
||||
all_summaries.extend(company_summaries)
|
||||
|
||||
def _sort_key(s: GZEndpointSummary) -> tuple:
|
||||
return (
|
||||
not s.infected,
|
||||
not s.signature_outdated,
|
||||
not s.product_outdated,
|
||||
s.name.lower(),
|
||||
)
|
||||
|
||||
all_summaries.sort(key=_sort_key)
|
||||
return all_summaries
|
||||
|
||||
|
||||
_gravityzone_service: Optional[GravityZoneService] = None
|
||||
|
||||
|
||||
def get_gravityzone_service() -> GravityZoneService:
|
||||
global _gravityzone_service
|
||||
if _gravityzone_service is None:
|
||||
_gravityzone_service = GravityZoneService()
|
||||
return _gravityzone_service
|
||||
810
bootstrap.ps1
Normal file
810
bootstrap.ps1
Normal file
@@ -0,0 +1,810 @@
|
||||
#Requires -RunAsAdministrator
|
||||
<#
|
||||
.SYNOPSIS
|
||||
ClaudeTools Bootstrap / Reinstall Script
|
||||
.DESCRIPTION
|
||||
One-and-done script to restore a complete ClaudeTools development environment
|
||||
after a fresh Windows 11 install. Idempotent - safe to re-run at any time.
|
||||
.NOTES
|
||||
Run from an elevated PowerShell prompt:
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||
.\bootstrap.ps1
|
||||
|
||||
Optional flags:
|
||||
-SkipPhase <number> Skip a specific phase (1-9)
|
||||
-OnlyPhase <number> Run only a specific phase (1-9)
|
||||
-Archive Create pre-reinstall archive instead of installing
|
||||
-ArchivePath <path> Custom archive output path (default: D:\ClaudeTools-backup.zip)
|
||||
#>
|
||||
|
||||
param(
|
||||
[int[]]$SkipPhase = @(),
|
||||
[int]$OnlyPhase = 0,
|
||||
[switch]$Archive,
|
||||
[string]$ArchivePath = "D:\ClaudeTools-backup.zip"
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Globals
|
||||
# ---------------------------------------------------------------------------
|
||||
$Script:ClaudeToolsRoot = "D:\ClaudeTools"
|
||||
$Script:GiteaRepo = "https://git.azcomputerguru.com/azcomputerguru/claudetools.git"
|
||||
$Script:ClaudeConfigDir = Join-Path $env:USERPROFILE ".claude"
|
||||
$Script:ClaudeCommandsDir = Join-Path $Script:ClaudeConfigDir "commands"
|
||||
$Script:MemoryDir = Join-Path $Script:ClaudeConfigDir "projects\D--ClaudeTools\memory"
|
||||
$Script:Errors = [System.Collections.Generic.List[string]]::new()
|
||||
$Script:Warnings = [System.Collections.Generic.List[string]]::new()
|
||||
$Script:Successes = [System.Collections.Generic.List[string]]::new()
|
||||
$Script:Skipped = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
function Write-Status {
|
||||
param(
|
||||
[string]$Prefix,
|
||||
[string]$Message,
|
||||
[ConsoleColor]$Color = [ConsoleColor]::White
|
||||
)
|
||||
Write-Host "[$Prefix] " -ForegroundColor $Color -NoNewline
|
||||
Write-Host $Message
|
||||
}
|
||||
|
||||
function Write-OK { param([string]$Msg) Write-Status "OK" $Msg Green; $Script:Successes.Add($Msg) }
|
||||
function Write-Err { param([string]$Msg) Write-Status "ERROR" $Msg Red; $Script:Errors.Add($Msg) }
|
||||
function Write-Warn { param([string]$Msg) Write-Status "WARNING" $Msg Yellow; $Script:Warnings.Add($Msg) }
|
||||
function Write-Skip { param([string]$Msg) Write-Status "SKIP" $Msg Cyan; $Script:Skipped.Add($Msg) }
|
||||
function Write-Info { param([string]$Msg) Write-Status "INFO" $Msg White }
|
||||
function Write-Phase { param([int]$Num, [string]$Title)
|
||||
Write-Host ""
|
||||
Write-Host ("=" * 70) -ForegroundColor Magenta
|
||||
Write-Host " Phase ${Num}: $Title" -ForegroundColor Magenta
|
||||
Write-Host ("=" * 70) -ForegroundColor Magenta
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
function Test-CommandExists {
|
||||
param([string]$Command)
|
||||
$null -ne (Get-Command $Command -ErrorAction SilentlyContinue)
|
||||
}
|
||||
|
||||
function Refresh-PathEnv {
|
||||
# Reload PATH from registry so newly-installed tools are visible
|
||||
$machinePath = [System.Environment]::GetEnvironmentVariable("Path", "Machine")
|
||||
$userPath = [System.Environment]::GetEnvironmentVariable("Path", "User")
|
||||
$env:Path = "$machinePath;$userPath"
|
||||
}
|
||||
|
||||
function Install-WingetPackage {
|
||||
param(
|
||||
[string]$PackageId,
|
||||
[string]$FriendlyName,
|
||||
[string]$VersionHint = ""
|
||||
)
|
||||
|
||||
# Check if already installed via winget list
|
||||
$installed = winget list --id $PackageId 2>$null
|
||||
if ($LASTEXITCODE -eq 0 -and $installed -match $PackageId) {
|
||||
Write-Skip "$FriendlyName is already installed"
|
||||
return $true
|
||||
}
|
||||
|
||||
Write-Info "Installing $FriendlyName ($PackageId)..."
|
||||
$args = @("install", "--id", $PackageId, "--accept-source-agreements", "--accept-package-agreements", "--silent")
|
||||
if ($VersionHint) {
|
||||
$args += @("--version", $VersionHint)
|
||||
}
|
||||
$result = & winget @args 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-OK "$FriendlyName installed successfully"
|
||||
return $true
|
||||
} else {
|
||||
# winget sometimes returns non-zero even on success (already installed race)
|
||||
$resultText = $result -join "`n"
|
||||
if ($resultText -match "already installed" -or $resultText -match "No applicable update") {
|
||||
Write-Skip "$FriendlyName is already installed"
|
||||
return $true
|
||||
}
|
||||
Write-Err "Failed to install $FriendlyName : $resultText"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function ShouldRunPhase {
|
||||
param([int]$PhaseNum)
|
||||
if ($OnlyPhase -gt 0) { return $PhaseNum -eq $OnlyPhase }
|
||||
return $PhaseNum -notin $SkipPhase
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Archive Mode: Create pre-reinstall backup
|
||||
# ---------------------------------------------------------------------------
|
||||
function Invoke-Archive {
|
||||
Write-Phase 0 "Creating Pre-Reinstall Archive"
|
||||
|
||||
$tempStaging = Join-Path $env:TEMP "claudetools-archive-staging"
|
||||
if (Test-Path $tempStaging) { Remove-Item $tempStaging -Recurse -Force }
|
||||
New-Item -ItemType Directory -Path $tempStaging -Force | Out-Null
|
||||
|
||||
# Create subdirectories in staging
|
||||
$ctDest = Join-Path $tempStaging "ClaudeTools"
|
||||
$memDest = Join-Path $tempStaging "claude-memory"
|
||||
|
||||
# Copy ClaudeTools repo (exclude node_modules, __pycache__, venv, .git large objects)
|
||||
Write-Info "Copying ClaudeTools repository..."
|
||||
if (Test-Path $Script:ClaudeToolsRoot) {
|
||||
$robocopyArgs = @(
|
||||
$Script:ClaudeToolsRoot, $ctDest,
|
||||
"/E", "/NFL", "/NDL", "/NJH", "/NJS",
|
||||
"/XD", "node_modules", "__pycache__", "venv", ".venv", ".mypy_cache"
|
||||
)
|
||||
& robocopy @robocopyArgs | Out-Null
|
||||
Write-OK "ClaudeTools repository copied"
|
||||
} else {
|
||||
Write-Err "ClaudeTools directory not found at $Script:ClaudeToolsRoot"
|
||||
}
|
||||
|
||||
# Copy Claude memory/config
|
||||
Write-Info "Copying Claude configuration and memory..."
|
||||
if (Test-Path $Script:ClaudeConfigDir) {
|
||||
$memSrc = $Script:ClaudeConfigDir
|
||||
& robocopy $memSrc (Join-Path $tempStaging "claude-config") /E /NFL /NDL /NJH /NJS /XD "node_modules" | Out-Null
|
||||
Write-OK "Claude configuration copied"
|
||||
} else {
|
||||
Write-Warn "Claude config directory not found at $Script:ClaudeConfigDir"
|
||||
}
|
||||
|
||||
# Compress
|
||||
Write-Info "Compressing archive to $ArchivePath ..."
|
||||
if (Test-Path $ArchivePath) { Remove-Item $ArchivePath -Force }
|
||||
Compress-Archive -Path "$tempStaging\*" -DestinationPath $ArchivePath -CompressionLevel Optimal
|
||||
Write-OK "Archive created: $ArchivePath"
|
||||
|
||||
# Cleanup staging
|
||||
Remove-Item $tempStaging -Recurse -Force
|
||||
|
||||
$sizeMB = [math]::Round((Get-Item $ArchivePath).Length / 1MB, 1)
|
||||
Write-Host ""
|
||||
Write-OK "Archive complete: $ArchivePath ($sizeMB MB)"
|
||||
Write-Info "Copy this file to external storage before reinstalling Windows."
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 1: Prerequisites
|
||||
# ---------------------------------------------------------------------------
|
||||
function Invoke-Phase1 {
|
||||
Write-Phase 1 "Prerequisites Installation (winget)"
|
||||
|
||||
if (-not (Test-CommandExists "winget")) {
|
||||
Write-Err "winget is not available. Please install App Installer from the Microsoft Store."
|
||||
return
|
||||
}
|
||||
|
||||
Install-WingetPackage "Git.Git" "Git"
|
||||
Install-WingetPackage "OpenJS.NodeJS" "Node.js (Latest LTS)"
|
||||
Install-WingetPackage "Python.Python.3.13" "Python 3.13"
|
||||
Install-WingetPackage "Ollama.Ollama" "Ollama"
|
||||
|
||||
# Refresh PATH so subsequent phases see the new installs
|
||||
Refresh-PathEnv
|
||||
|
||||
# Verify critical tools are now on PATH
|
||||
foreach ($tool in @("git", "node", "python", "ollama")) {
|
||||
if (Test-CommandExists $tool) {
|
||||
$ver = & $tool --version 2>&1 | Select-Object -First 1
|
||||
Write-OK "$tool found: $ver"
|
||||
} else {
|
||||
Write-Warn "$tool not found on PATH after install. You may need to restart your terminal."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 2: Claude Code + Global NPM Packages
|
||||
# ---------------------------------------------------------------------------
|
||||
function Invoke-Phase2 {
|
||||
Write-Phase 2 "Claude Code and Global NPM Packages"
|
||||
|
||||
if (-not (Test-CommandExists "npm")) {
|
||||
Refresh-PathEnv
|
||||
if (-not (Test-CommandExists "npm")) {
|
||||
Write-Err "npm not found. Node.js installation may have failed."
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
$npmPackages = @(
|
||||
@{ Name = "@anthropic-ai/claude-code"; Cmd = "claude" },
|
||||
@{ Name = "clawhub"; Cmd = "clawhub" },
|
||||
@{ Name = "mcporter"; Cmd = "mcporter" },
|
||||
@{ Name = "openclaw"; Cmd = "openclaw" }
|
||||
)
|
||||
|
||||
foreach ($pkg in $npmPackages) {
|
||||
# Check if already installed globally
|
||||
$installed = npm list -g $($pkg.Name) --depth=0 2>$null
|
||||
if ($LASTEXITCODE -eq 0 -and $installed -match $pkg.Name) {
|
||||
Write-Skip "$($pkg.Name) is already installed globally"
|
||||
} else {
|
||||
Write-Info "Installing $($pkg.Name) globally..."
|
||||
npm install -g $($pkg.Name) 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-OK "$($pkg.Name) installed"
|
||||
} else {
|
||||
Write-Err "Failed to install $($pkg.Name)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 3: Clone or Configure ClaudeTools Repository
|
||||
# ---------------------------------------------------------------------------
|
||||
function Invoke-Phase3 {
|
||||
Write-Phase 3 "ClaudeTools Repository"
|
||||
|
||||
if (-not (Test-CommandExists "git")) {
|
||||
Refresh-PathEnv
|
||||
if (-not (Test-CommandExists "git")) {
|
||||
Write-Err "git not found. Cannot proceed with repository setup."
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path (Join-Path $Script:ClaudeToolsRoot ".git")) {
|
||||
Write-Skip "ClaudeTools repository already exists at $Script:ClaudeToolsRoot"
|
||||
Write-Info "Configuring git for self-signed certificate..."
|
||||
Push-Location $Script:ClaudeToolsRoot
|
||||
git config http.sslVerify false 2>&1 | Out-Null
|
||||
Pop-Location
|
||||
Write-OK "Git SSL verification disabled for self-signed Gitea cert"
|
||||
|
||||
# Ensure remote is correct
|
||||
Push-Location $Script:ClaudeToolsRoot
|
||||
$currentRemote = git remote get-url origin 2>$null
|
||||
if ($currentRemote -ne $Script:GiteaRepo) {
|
||||
git remote set-url origin $Script:GiteaRepo 2>&1 | Out-Null
|
||||
Write-OK "Git remote updated to $Script:GiteaRepo"
|
||||
} else {
|
||||
Write-OK "Git remote is correct"
|
||||
}
|
||||
Pop-Location
|
||||
}
|
||||
elseif (Test-Path $Script:ClaudeToolsRoot) {
|
||||
# Directory exists but is not a git repo (restored from archive)
|
||||
Write-Info "Directory exists but is not a git repo. Initializing..."
|
||||
Push-Location $Script:ClaudeToolsRoot
|
||||
git init 2>&1 | Out-Null
|
||||
git remote add origin $Script:GiteaRepo 2>&1 | Out-Null
|
||||
git config http.sslVerify false 2>&1 | Out-Null
|
||||
git fetch origin 2>&1 | Out-Null
|
||||
git checkout -B main origin/main 2>&1 | Out-Null
|
||||
Pop-Location
|
||||
Write-OK "Repository initialized from archive and connected to remote"
|
||||
}
|
||||
else {
|
||||
# Fresh clone
|
||||
Write-Info "Cloning ClaudeTools repository..."
|
||||
# Ensure D:\ exists
|
||||
if (-not (Test-Path "D:\")) {
|
||||
Write-Err "D:\ drive not found. Cannot clone repository."
|
||||
return
|
||||
}
|
||||
git clone -c http.sslVerify=false $Script:GiteaRepo $Script:ClaudeToolsRoot 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Push-Location $Script:ClaudeToolsRoot
|
||||
git config http.sslVerify false 2>&1 | Out-Null
|
||||
Pop-Location
|
||||
Write-OK "Repository cloned successfully"
|
||||
} else {
|
||||
Write-Err "Failed to clone repository"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 4: Python Environment
|
||||
# ---------------------------------------------------------------------------
|
||||
function Invoke-Phase4 {
|
||||
Write-Phase 4 "Python Packages (Global pip install)"
|
||||
|
||||
if (-not (Test-CommandExists "python")) {
|
||||
Refresh-PathEnv
|
||||
if (-not (Test-CommandExists "python")) {
|
||||
Write-Err "python not found. Python installation may have failed."
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
$pyVersion = python --version 2>&1
|
||||
Write-OK "Python detected: $pyVersion"
|
||||
|
||||
# Full list of required packages with pinned versions
|
||||
$pipPackages = @(
|
||||
"alembic==1.13.1",
|
||||
"annotated-types==0.7.0",
|
||||
"anyio==4.12.1",
|
||||
"argon2-cffi==25.1.0",
|
||||
"argon2-cffi-bindings==25.1.0",
|
||||
"bcrypt==5.0.0",
|
||||
"beautifulsoup4==4.13.5",
|
||||
"certifi==2026.1.4",
|
||||
"cffi==2.0.0",
|
||||
"claude-agent-sdk==0.1.19",
|
||||
"claude-code-sdk==0.0.25",
|
||||
"click==8.3.1",
|
||||
"colorama==0.4.6",
|
||||
"compressed-rtf==1.0.6",
|
||||
"cryptography==46.0.3",
|
||||
"easygui==0.98.3",
|
||||
"extract-msg==0.55.0",
|
||||
"fastapi==0.128.0",
|
||||
"google-api-core==2.30.0",
|
||||
"google-api-python-client==2.192.0",
|
||||
"google-auth==2.49.0",
|
||||
"google-auth-httplib2==0.3.0",
|
||||
"googleapis-common-protos==1.73.0",
|
||||
"greenlet==3.3.0",
|
||||
"httpcore==1.0.9",
|
||||
"httplib2==0.31.2",
|
||||
"httpx==0.28.1",
|
||||
"httpx-sse==0.4.3",
|
||||
"invoke==2.2.1",
|
||||
"jsonschema==4.26.0",
|
||||
"lark==1.3.1",
|
||||
"Mako==1.3.10",
|
||||
"MarkupSafe==3.0.3",
|
||||
"mcp==1.25.0",
|
||||
"msoffcrypto-tool==5.4.2",
|
||||
"numpy==2.4.2",
|
||||
"olefile==0.47",
|
||||
"oletools==0.60.2",
|
||||
"opencv-python==4.13.0.90",
|
||||
"paramiko==4.0.0",
|
||||
"passlib==1.7.4",
|
||||
"pillow==12.1.0",
|
||||
"proto-plus==1.27.1",
|
||||
"protobuf==6.33.5",
|
||||
"pydantic==2.12.5",
|
||||
"pydantic-settings==2.12.0",
|
||||
"PyJWT==2.10.1",
|
||||
"PyMySQL==1.1.0",
|
||||
"PyNaCl==1.6.2",
|
||||
"python-dotenv==1.2.1",
|
||||
"python-multipart==0.0.21",
|
||||
"pywin32==311",
|
||||
"pyzbar==0.1.9",
|
||||
"requests==2.32.5",
|
||||
"RTFDE==0.1.2.2",
|
||||
"SQLAlchemy==2.0.45",
|
||||
"sse-starlette==3.1.2",
|
||||
"starlette==0.50.0",
|
||||
"tzdata==2025.3",
|
||||
"tzlocal==5.3.1",
|
||||
"uritemplate==4.2.0",
|
||||
"uvicorn==0.40.0",
|
||||
"uv==0.9.9",
|
||||
"websockets==15.0.1"
|
||||
)
|
||||
|
||||
# Write a temporary requirements file for batch install
|
||||
$reqFile = Join-Path $env:TEMP "claudetools-requirements.txt"
|
||||
$pipPackages | Out-File -FilePath $reqFile -Encoding UTF8
|
||||
|
||||
Write-Info "Installing $($pipPackages.Count) Python packages (this may take several minutes)..."
|
||||
$pipOutput = python -m pip install --upgrade pip 2>&1
|
||||
$pipOutput = python -m pip install -r $reqFile 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-OK "All Python packages installed successfully"
|
||||
} else {
|
||||
# Check for partial failures
|
||||
$failLines = ($pipOutput | Select-String "ERROR:" | ForEach-Object { $_.Line })
|
||||
if ($failLines) {
|
||||
foreach ($line in $failLines) {
|
||||
Write-Err "pip: $line"
|
||||
}
|
||||
} else {
|
||||
Write-Warn "pip exited with warnings but may have succeeded. Review output above."
|
||||
}
|
||||
}
|
||||
|
||||
Remove-Item $reqFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 5: Ollama Models
|
||||
# ---------------------------------------------------------------------------
|
||||
function Invoke-Phase5 {
|
||||
Write-Phase 5 "Ollama Models"
|
||||
|
||||
if (-not (Test-CommandExists "ollama")) {
|
||||
Refresh-PathEnv
|
||||
if (-not (Test-CommandExists "ollama")) {
|
||||
Write-Err "ollama not found. Ollama installation may have failed."
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
# Ensure Ollama service is running
|
||||
Write-Info "Ensuring Ollama service is running..."
|
||||
$ollamaProcess = Get-Process "ollama" -ErrorAction SilentlyContinue
|
||||
if (-not $ollamaProcess) {
|
||||
Write-Info "Starting Ollama service..."
|
||||
Start-Process "ollama" -ArgumentList "serve" -WindowStyle Hidden
|
||||
Start-Sleep -Seconds 3
|
||||
}
|
||||
|
||||
$models = @(
|
||||
"nomic-embed-text",
|
||||
"llama3.1:8b",
|
||||
"qwen2.5-coder:7b"
|
||||
)
|
||||
|
||||
foreach ($model in $models) {
|
||||
Write-Info "Pulling model: $model (this may take a while)..."
|
||||
$pullOutput = ollama pull $model 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-OK "Model $model pulled successfully"
|
||||
} else {
|
||||
$outputText = $pullOutput -join "`n"
|
||||
if ($outputText -match "up to date") {
|
||||
Write-Skip "Model $model is already up to date"
|
||||
} else {
|
||||
Write-Err "Failed to pull model $model : $outputText"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 6: MCP Server Setup
|
||||
# ---------------------------------------------------------------------------
|
||||
function Invoke-Phase6 {
|
||||
Write-Phase 6 "MCP Server Setup (Ollama Assistant)"
|
||||
|
||||
$ollamaAssistantDir = Join-Path $Script:ClaudeToolsRoot "mcp-servers\ollama-assistant"
|
||||
$venvDir = Join-Path $ollamaAssistantDir "venv"
|
||||
$venvPython = Join-Path $venvDir "Scripts\python.exe"
|
||||
$requirementsFile = Join-Path $ollamaAssistantDir "requirements.txt"
|
||||
|
||||
if (-not (Test-Path $ollamaAssistantDir)) {
|
||||
Write-Err "Ollama assistant directory not found at $ollamaAssistantDir"
|
||||
return
|
||||
}
|
||||
|
||||
# Create venv if it doesn't exist or is broken
|
||||
if (-not (Test-Path $venvPython)) {
|
||||
Write-Info "Creating Python virtual environment..."
|
||||
if (Test-Path $venvDir) { Remove-Item $venvDir -Recurse -Force }
|
||||
python -m venv $venvDir 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-OK "Virtual environment created"
|
||||
} else {
|
||||
Write-Err "Failed to create virtual environment"
|
||||
return
|
||||
}
|
||||
} else {
|
||||
Write-Skip "Virtual environment already exists"
|
||||
}
|
||||
|
||||
# Install requirements
|
||||
Write-Info "Installing MCP server dependencies..."
|
||||
if (Test-Path $requirementsFile) {
|
||||
& $venvPython -m pip install --upgrade pip 2>&1 | Out-Null
|
||||
$installOutput = & $venvPython -m pip install -r $requirementsFile 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-OK "MCP server dependencies installed"
|
||||
} else {
|
||||
Write-Err "Failed to install MCP server dependencies: $installOutput"
|
||||
}
|
||||
} else {
|
||||
# Fallback: install mcp and httpx directly
|
||||
& $venvPython -m pip install --upgrade pip 2>&1 | Out-Null
|
||||
& $venvPython -m pip install "mcp>=0.1.0" "httpx>=0.25.0" 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-OK "MCP server dependencies installed (fallback)"
|
||||
} else {
|
||||
Write-Err "Failed to install MCP server dependencies"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify .mcp.json exists in repo
|
||||
$mcpJson = Join-Path $Script:ClaudeToolsRoot ".mcp.json"
|
||||
if (Test-Path $mcpJson) {
|
||||
Write-OK ".mcp.json configuration found in repository"
|
||||
} else {
|
||||
Write-Warn ".mcp.json not found - MCP servers will not be configured"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 7: Claude Code Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
function Invoke-Phase7 {
|
||||
Write-Phase 7 "Claude Code Configuration"
|
||||
|
||||
# Create directories
|
||||
foreach ($dir in @($Script:ClaudeConfigDir, $Script:ClaudeCommandsDir, $Script:MemoryDir)) {
|
||||
if (-not (Test-Path $dir)) {
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
Write-OK "Created directory: $dir"
|
||||
} else {
|
||||
Write-Skip "Directory exists: $dir"
|
||||
}
|
||||
}
|
||||
|
||||
# Write settings.json
|
||||
$settingsPath = Join-Path $Script:ClaudeConfigDir "settings.json"
|
||||
$settingsContent = @'
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git:*)", "Bash(gh:*)", "Bash(ssh:*)", "Bash(scp:*)", "Bash(rsync:*)",
|
||||
"Bash(wsl:*)", "Bash(wsl.exe:*)", "Bash(cat:*)", "Bash(ls:*)", "Bash(find:*)",
|
||||
"Bash(grep:*)", "Bash(echo:*)", "Bash(chmod:*)", "Bash(chown:*)", "Bash(mkdir:*)",
|
||||
"Bash(rm:*)", "Bash(cp:*)", "Bash(mv:*)", "Bash(curl:*)", "Bash(wget:*)",
|
||||
"Bash(nslookup:*)", "Bash(dig:*)", "Bash(ping:*)", "Bash(python:*)", "Bash(python3:*)",
|
||||
"Bash(node:*)", "Bash(npm:*)", "Bash(npx:*)", "Bash(cargo:*)", "Bash(rustc:*)",
|
||||
"Bash(rustup:*)", "Bash(powershell:*)", "Bash(powershell.exe:*)", "Bash(pwsh:*)",
|
||||
"Bash(which:*)", "Bash(where:*)", "Bash(whoami:*)", "Bash(date:*)", "Bash(head:*)",
|
||||
"Bash(tail:*)", "Bash(less:*)", "Bash(more:*)", "Bash(diff:*)", "Bash(tar:*)",
|
||||
"Bash(unzip:*)", "Bash(zip:*)", "Bash(docker:*)", "Bash(docker-compose:*)",
|
||||
"Bash(systemctl:*)", "Bash(service:*)", "Bash(journalctl:*)", "Bash(apt:*)",
|
||||
"Bash(apt-get:*)", "Bash(brew:*)", "Bash(code:*)", "Bash(make:*)", "Bash(cmake:*)",
|
||||
"Bash(dir:*)", "Bash(wc:*)", "Bash(winget:*)", "Bash(choco:*)", "Bash(ipconfig:*)",
|
||||
"Bash(net:*)", "Bash(perl:*)", "Bash(xxd:*)", "Bash(timeout:*)", "Bash(claude:*)",
|
||||
"Bash(plink:*)", "WebFetch(domain:*)", "Skill(s)",
|
||||
"Read(//c/Users/$env:USERNAME/.claude/**)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
},
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "input=$(cat); remaining=$(echo \"$input\" | jq -r '.context_window.remaining_percentage // empty'); [ -n \"$remaining\" ] && printf \"Context: %.0f%% remaining\" \"$remaining\" || echo \"\""
|
||||
},
|
||||
"skipDangerousModePermissionPrompt": true
|
||||
}
|
||||
'@
|
||||
|
||||
# Replace $env:USERNAME placeholder with actual username
|
||||
$settingsContent = $settingsContent -replace '\$env:USERNAME', $env:USERNAME
|
||||
|
||||
Set-Content -Path $settingsPath -Value $settingsContent -Encoding UTF8 -Force
|
||||
Write-OK "settings.json written to $settingsPath"
|
||||
|
||||
# Copy commands from repo to global
|
||||
$repoCommandsDir = Join-Path $Script:ClaudeToolsRoot ".claude\commands"
|
||||
if (Test-Path $repoCommandsDir) {
|
||||
$commandFiles = Get-ChildItem -Path $repoCommandsDir -File
|
||||
if ($commandFiles.Count -gt 0) {
|
||||
Copy-Item -Path "$repoCommandsDir\*" -Destination $Script:ClaudeCommandsDir -Force
|
||||
Write-OK "Copied $($commandFiles.Count) command files to $Script:ClaudeCommandsDir"
|
||||
} else {
|
||||
Write-Warn "No command files found in $repoCommandsDir"
|
||||
}
|
||||
} else {
|
||||
Write-Warn "Repo commands directory not found at $repoCommandsDir (run after Phase 3)"
|
||||
}
|
||||
|
||||
# Check for archived memory to restore
|
||||
Write-Info "Checking for memory directory..."
|
||||
if (Test-Path (Join-Path $Script:MemoryDir "MEMORY.md")) {
|
||||
Write-Skip "Memory files already present"
|
||||
} else {
|
||||
Write-Warn "Memory directory is empty. If you have an archive, manually restore:"
|
||||
Write-Warn " Copy contents to $Script:MemoryDir"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 8: GrepAI Setup
|
||||
# ---------------------------------------------------------------------------
|
||||
function Invoke-Phase8 {
|
||||
Write-Phase 8 "GrepAI Setup"
|
||||
|
||||
$grepaiExe = Join-Path $Script:ClaudeToolsRoot "grepai.exe"
|
||||
|
||||
if (-not (Test-Path $grepaiExe)) {
|
||||
Write-Err "grepai.exe not found at $grepaiExe"
|
||||
Write-Warn "GrepAI binary should be in the repository. Ensure Phase 3 completed successfully."
|
||||
return
|
||||
}
|
||||
|
||||
Write-OK "grepai.exe found at $grepaiExe"
|
||||
|
||||
# Check if already initialized
|
||||
$grepaiConfig = Join-Path $Script:ClaudeToolsRoot ".grepai"
|
||||
if (Test-Path $grepaiConfig) {
|
||||
Write-Skip "GrepAI appears to already be initialized (.grepai directory exists)"
|
||||
} else {
|
||||
Write-Info "Initializing GrepAI..."
|
||||
Write-Info "This may prompt for configuration. Accept defaults for Ollama + nomic-embed-text."
|
||||
Push-Location $Script:ClaudeToolsRoot
|
||||
& $grepaiExe init 2>&1
|
||||
Pop-Location
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-OK "GrepAI initialized"
|
||||
} else {
|
||||
Write-Warn "GrepAI init may require manual interaction. Run manually:"
|
||||
Write-Warn " cd $Script:ClaudeToolsRoot && .\grepai.exe init"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 9: Verification
|
||||
# ---------------------------------------------------------------------------
|
||||
function Invoke-Phase9 {
|
||||
Write-Phase 9 "Verification"
|
||||
|
||||
$checks = @(
|
||||
@{ Name = "Git"; Cmd = "git --version" },
|
||||
@{ Name = "Node.js"; Cmd = "node --version" },
|
||||
@{ Name = "npm"; Cmd = "npm --version" },
|
||||
@{ Name = "Python"; Cmd = "python --version" },
|
||||
@{ Name = "pip"; Cmd = "python -m pip --version" },
|
||||
@{ Name = "Ollama"; Cmd = "ollama --version" },
|
||||
@{ Name = "Claude Code CLI"; Cmd = "claude --version" }
|
||||
)
|
||||
|
||||
Refresh-PathEnv
|
||||
|
||||
foreach ($check in $checks) {
|
||||
try {
|
||||
$result = Invoke-Expression $check.Cmd 2>&1 | Select-Object -First 1
|
||||
if ($LASTEXITCODE -eq 0 -or $result) {
|
||||
Write-OK "$($check.Name): $result"
|
||||
} else {
|
||||
Write-Err "$($check.Name): not found or not working"
|
||||
}
|
||||
} catch {
|
||||
Write-Err "$($check.Name): $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# Check directories
|
||||
Write-Host ""
|
||||
Write-Info "Directory checks:"
|
||||
$dirs = @(
|
||||
$Script:ClaudeToolsRoot,
|
||||
$Script:ClaudeConfigDir,
|
||||
$Script:ClaudeCommandsDir,
|
||||
(Join-Path $Script:ClaudeToolsRoot "mcp-servers\ollama-assistant\venv")
|
||||
)
|
||||
foreach ($dir in $dirs) {
|
||||
if (Test-Path $dir) {
|
||||
Write-OK "EXISTS: $dir"
|
||||
} else {
|
||||
Write-Err "MISSING: $dir"
|
||||
}
|
||||
}
|
||||
|
||||
# Check key files
|
||||
Write-Host ""
|
||||
Write-Info "File checks:"
|
||||
$files = @(
|
||||
(Join-Path $Script:ClaudeConfigDir "settings.json"),
|
||||
(Join-Path $Script:ClaudeToolsRoot ".mcp.json"),
|
||||
(Join-Path $Script:ClaudeToolsRoot "grepai.exe")
|
||||
)
|
||||
foreach ($file in $files) {
|
||||
if (Test-Path $file) {
|
||||
Write-OK "EXISTS: $file"
|
||||
} else {
|
||||
Write-Err "MISSING: $file"
|
||||
}
|
||||
}
|
||||
|
||||
# Check Ollama models
|
||||
Write-Host ""
|
||||
Write-Info "Ollama model checks:"
|
||||
if (Test-CommandExists "ollama") {
|
||||
$modelList = ollama list 2>&1
|
||||
foreach ($model in @("nomic-embed-text", "llama3.1:8b", "qwen2.5-coder:7b")) {
|
||||
if ($modelList -match [regex]::Escape($model)) {
|
||||
Write-OK "Model loaded: $model"
|
||||
} else {
|
||||
Write-Warn "Model not found: $model"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Warn "Cannot check Ollama models - ollama not on PATH"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main Execution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "================================================================" -ForegroundColor Cyan
|
||||
Write-Host " ClaudeTools Bootstrap / Reinstall Script" -ForegroundColor Cyan
|
||||
Write-Host " $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Cyan
|
||||
Write-Host "================================================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Archive mode
|
||||
if ($Archive) {
|
||||
Invoke-Archive
|
||||
return
|
||||
}
|
||||
|
||||
# Check for admin
|
||||
$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
if (-not $isAdmin) {
|
||||
Write-Err "This script requires administrator privileges for winget installations."
|
||||
Write-Err "Re-run from an elevated PowerShell prompt."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Execute phases
|
||||
$phases = @(
|
||||
@{ Num = 1; Func = { Invoke-Phase1 } },
|
||||
@{ Num = 2; Func = { Invoke-Phase2 } },
|
||||
@{ Num = 3; Func = { Invoke-Phase3 } },
|
||||
@{ Num = 4; Func = { Invoke-Phase4 } },
|
||||
@{ Num = 5; Func = { Invoke-Phase5 } },
|
||||
@{ Num = 6; Func = { Invoke-Phase6 } },
|
||||
@{ Num = 7; Func = { Invoke-Phase7 } },
|
||||
@{ Num = 8; Func = { Invoke-Phase8 } },
|
||||
@{ Num = 9; Func = { Invoke-Phase9 } }
|
||||
)
|
||||
|
||||
foreach ($phase in $phases) {
|
||||
if (ShouldRunPhase $phase.Num) {
|
||||
try {
|
||||
& $phase.Func
|
||||
} catch {
|
||||
Write-Err "Phase $($phase.Num) failed with exception: $($_.Exception.Message)"
|
||||
}
|
||||
} else {
|
||||
Write-Skip "Phase $($phase.Num) skipped (by request)"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
Write-Host ""
|
||||
Write-Host "================================================================" -ForegroundColor Cyan
|
||||
Write-Host " Bootstrap Summary" -ForegroundColor Cyan
|
||||
Write-Host "================================================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
if ($Script:Successes.Count -gt 0) {
|
||||
Write-Host " Successes: $($Script:Successes.Count)" -ForegroundColor Green
|
||||
}
|
||||
if ($Script:Skipped.Count -gt 0) {
|
||||
Write-Host " Skipped: $($Script:Skipped.Count)" -ForegroundColor Cyan
|
||||
}
|
||||
if ($Script:Warnings.Count -gt 0) {
|
||||
Write-Host " Warnings: $($Script:Warnings.Count)" -ForegroundColor Yellow
|
||||
foreach ($w in $Script:Warnings) {
|
||||
Write-Host " - $w" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
if ($Script:Errors.Count -gt 0) {
|
||||
Write-Host " Errors: $($Script:Errors.Count)" -ForegroundColor Red
|
||||
foreach ($e in $Script:Errors) {
|
||||
Write-Host " - $e" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " Manual steps remaining:" -ForegroundColor Yellow
|
||||
Write-Host " 1. Run 'claude' and authenticate with Anthropic API key" -ForegroundColor Yellow
|
||||
Write-Host " 2. Install Claude-in-Chrome browser extension" -ForegroundColor Yellow
|
||||
Write-Host " 3. Set GITHUB_PERSONAL_ACCESS_TOKEN in .mcp.json" -ForegroundColor Yellow
|
||||
Write-Host " 4. Restore memory files if not already present" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
if ($Script:Errors.Count -eq 0) {
|
||||
Write-Host " [OK] Bootstrap completed successfully!" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " [WARNING] Bootstrap completed with $($Script:Errors.Count) error(s). Review above." -ForegroundColor Yellow
|
||||
}
|
||||
Write-Host ""
|
||||
28
clients/_client_template/cloud/azure.md
Normal file
28
clients/_client_template/cloud/azure.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Azure / Cloud Services
|
||||
|
||||
## Azure Subscription
|
||||
- Subscription Name:
|
||||
- Subscription ID:
|
||||
- Resource Group(s):
|
||||
- Region:
|
||||
- Monthly Spend (approx):
|
||||
|
||||
## Virtual Machines
|
||||
| VM Name | Size | OS | IP | Purpose |
|
||||
|---------------|------------|------------|------------|-----------------|
|
||||
| | | | | |
|
||||
|
||||
## Networking
|
||||
- Virtual Network:
|
||||
- Address Space:
|
||||
- Subnets:
|
||||
- VPN Gateway to On-Prem: Yes/No
|
||||
- ExpressRoute: Yes/No
|
||||
|
||||
## Other Cloud Services
|
||||
<!-- AWS, Google Workspace, third-party SaaS -->
|
||||
| Service | Purpose | Admin URL | Notes |
|
||||
|-----------------|------------------|------------------|-----------------|
|
||||
| | | | |
|
||||
|
||||
## Notes
|
||||
52
clients/_client_template/cloud/m365.md
Normal file
52
clients/_client_template/cloud/m365.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Microsoft 365
|
||||
|
||||
## Tenant Info
|
||||
- Tenant Name:
|
||||
- Tenant ID:
|
||||
- Primary Domain:
|
||||
- Admin Portal URL: https://admin.microsoft.com
|
||||
|
||||
## Licensing
|
||||
| License Type | Quantity | Assigned | Available |
|
||||
|--------------------------|----------|----------|-----------|
|
||||
| Microsoft 365 Business Basic | | | |
|
||||
| Microsoft 365 Business Standard | | | |
|
||||
| Microsoft 365 Business Premium | | | |
|
||||
| Exchange Online Plan 1/2 | | | |
|
||||
| Other | | | |
|
||||
|
||||
## Exchange Online
|
||||
- Mail Domain(s):
|
||||
- MX Record Points To:
|
||||
- SPF Record:
|
||||
- DKIM Enabled: Yes/No
|
||||
- DMARC Policy:
|
||||
- Shared Mailboxes:
|
||||
- Distribution Groups:
|
||||
- Mail Flow Rules: Yes/No (describe below)
|
||||
|
||||
## SharePoint / OneDrive
|
||||
- SharePoint Sites:
|
||||
- External Sharing: Enabled/Disabled
|
||||
- OneDrive Storage Limit:
|
||||
|
||||
## Teams
|
||||
- Teams Phone System: Yes/No
|
||||
- Calling Plan / Direct Routing:
|
||||
- Auto Attendant:
|
||||
|
||||
## Entra ID (Azure AD)
|
||||
- Hybrid Joined: Yes/No
|
||||
- Azure AD Connect Server:
|
||||
- Sync Schedule:
|
||||
- Password Hash Sync: Yes/No
|
||||
- MFA Enforced: Yes/No
|
||||
- Conditional Access Policies:
|
||||
|
||||
## Security
|
||||
- Defender for Office 365: Yes/No
|
||||
- Safe Links: Yes/No
|
||||
- Safe Attachments: Yes/No
|
||||
- Audit Log Retention:
|
||||
|
||||
## Notes
|
||||
19
clients/_client_template/issues/log.md
Normal file
19
clients/_client_template/issues/log.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Issue Log
|
||||
|
||||
Record past issues and their resolutions here. This helps the AI learn from historical
|
||||
troubleshooting and avoid repeating failed approaches.
|
||||
|
||||
## Template
|
||||
|
||||
### [DATE] - [Brief Description]
|
||||
- **Reported By:**
|
||||
- **Severity:** Low / Medium / High / Critical
|
||||
- **Symptoms:**
|
||||
- **Root Cause:**
|
||||
- **Resolution:**
|
||||
- **Time to Resolve:**
|
||||
- **Lessons Learned:**
|
||||
|
||||
---
|
||||
|
||||
<!-- Add new issues above this line, newest first -->
|
||||
31
clients/_client_template/network/dhcp.md
Normal file
31
clients/_client_template/network/dhcp.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# DHCP Configuration
|
||||
|
||||
## DHCP Server
|
||||
- Server Name:
|
||||
- Server IP:
|
||||
- Failover Partner:
|
||||
|
||||
## Scopes
|
||||
|
||||
### Scope - [VLAN Name]
|
||||
- Subnet:
|
||||
- Range Start:
|
||||
- Range End:
|
||||
- Subnet Mask:
|
||||
- Default Gateway:
|
||||
- DNS Servers:
|
||||
- Lease Duration:
|
||||
- Exclusions:
|
||||
|
||||
<!-- Copy the block above for each DHCP scope -->
|
||||
|
||||
## Reservations
|
||||
| Device Name | MAC Address | IP Address | Scope | Notes |
|
||||
|-----------------|-------------------|-----------------|---------------|---------------|
|
||||
| | | | | |
|
||||
|
||||
## DHCP Relay
|
||||
- Relay agents configured on:
|
||||
- Helper address:
|
||||
|
||||
## Notes
|
||||
33
clients/_client_template/network/dns.md
Normal file
33
clients/_client_template/network/dns.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# DNS Configuration
|
||||
|
||||
## Internal DNS Servers
|
||||
| Server Name | IP Address | Role |
|
||||
|-------------|-----------|-------------------|
|
||||
| | | Primary |
|
||||
| | | Secondary |
|
||||
|
||||
## DNS Forwarders
|
||||
- Forwarder 1:
|
||||
- Forwarder 2:
|
||||
|
||||
## Conditional Forwarders
|
||||
| Domain | Forward To | Purpose |
|
||||
|----------------------|-----------------|-------------------|
|
||||
| | | |
|
||||
|
||||
## Key DNS Records
|
||||
| Record Type | Name | Value | Notes |
|
||||
|-------------|------------------|------------------|------------------|
|
||||
| A | | | |
|
||||
| CNAME | | | |
|
||||
| MX | | | |
|
||||
| TXT | | | |
|
||||
|
||||
## External DNS
|
||||
- Registrar:
|
||||
- Hosted At:
|
||||
- Primary Domain:
|
||||
- Management URL:
|
||||
|
||||
## Notes
|
||||
<!-- Split-brain DNS, special zones, etc. -->
|
||||
47
clients/_client_template/network/firewall.md
Normal file
47
clients/_client_template/network/firewall.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Firewall Configuration
|
||||
|
||||
## Device Info
|
||||
- Vendor/Model:
|
||||
- Firmware Version:
|
||||
- Management IP:
|
||||
- Management URL:
|
||||
- HA Pair: Yes/No
|
||||
- License Expiry:
|
||||
|
||||
## Interfaces
|
||||
| Interface | Zone | IP Address | VLAN | Description |
|
||||
|-----------|-----------|-----------------|------|-------------------|
|
||||
| WAN1 | WAN | | | Primary Internet |
|
||||
| WAN2 | WAN | | | Backup Internet |
|
||||
| LAN | LAN | | | |
|
||||
| DMZ | DMZ | | | |
|
||||
|
||||
## NAT Rules
|
||||
| Name | Source | Destination | Port(s) | NAT To |
|
||||
|-------------------|---------------|----------------|-------------|-----------------|
|
||||
| | | | | |
|
||||
|
||||
## Key Firewall Policies
|
||||
| Name | Source Zone | Dest Zone | Service | Action | Notes |
|
||||
|-------------------|--------------|---------------|-------------|--------|--------|
|
||||
| | | | | | |
|
||||
|
||||
## VPN
|
||||
### Site-to-Site VPNs
|
||||
| Peer Name | Peer IP | Local Subnet | Remote Subnet | Status |
|
||||
|-------------------|--------------|----------------|---------------|--------|
|
||||
| | | | | |
|
||||
|
||||
### SSL/Client VPN
|
||||
- Enabled: Yes/No
|
||||
- Portal URL:
|
||||
- Auth Method:
|
||||
- IP Pool:
|
||||
- Split Tunnel: Yes/No
|
||||
|
||||
## Content Filtering
|
||||
- Web Filter Profile:
|
||||
- App Control Profile:
|
||||
- DNS Filter:
|
||||
|
||||
## Notes
|
||||
43
clients/_client_template/network/topology.md
Normal file
43
clients/_client_template/network/topology.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Network Topology
|
||||
|
||||
## Internet Connection
|
||||
- ISP:
|
||||
- Circuit Type:
|
||||
- Speed (Down/Up):
|
||||
- Public IP:
|
||||
- Gateway:
|
||||
- Modem Model:
|
||||
|
||||
## Core Switch
|
||||
- Model:
|
||||
- IP Address:
|
||||
- Management URL:
|
||||
- Firmware Version:
|
||||
- Location:
|
||||
|
||||
## Additional Switches
|
||||
<!-- Copy this block for each switch -->
|
||||
### Switch - [Name/Location]
|
||||
- Model:
|
||||
- IP Address:
|
||||
- Port Count:
|
||||
- PoE: Yes/No
|
||||
- Uplink To:
|
||||
|
||||
## Wireless
|
||||
- Controller Model:
|
||||
- Controller IP:
|
||||
- Number of APs:
|
||||
- AP Model(s):
|
||||
|
||||
### Access Points
|
||||
<!-- Copy for each AP -->
|
||||
- AP Name:
|
||||
- Location:
|
||||
- IP Address:
|
||||
- Connected Switch/Port:
|
||||
|
||||
## WAN / SD-WAN
|
||||
- SD-WAN Vendor:
|
||||
- Number of Sites:
|
||||
- Hub Site:
|
||||
21
clients/_client_template/network/vlans.md
Normal file
21
clients/_client_template/network/vlans.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# VLANs
|
||||
|
||||
## VLAN Table
|
||||
|
||||
| VLAN ID | Name | Subnet | Gateway | DHCP Scope | Purpose |
|
||||
|---------|---------------|-----------------|-----------------|------------------|------------------------|
|
||||
| 1 | Default | | | | |
|
||||
| 10 | Management | | | | Network devices |
|
||||
| 20 | Servers | | | | Server infrastructure |
|
||||
| 30 | Workstations | | | | End user devices |
|
||||
| 40 | VoIP | | | | Phone system |
|
||||
| 50 | WiFi-Corp | | | | Corporate wireless |
|
||||
| 60 | WiFi-Guest | | | | Guest wireless |
|
||||
| 100 | Security | | | | Cameras / access ctrl |
|
||||
|
||||
## Inter-VLAN Routing
|
||||
- Performed by:
|
||||
- Routing device IP:
|
||||
|
||||
## VLAN Notes
|
||||
<!-- Any special considerations, trunk ports, tagged/untagged config -->
|
||||
31
clients/_client_template/overview.md
Normal file
31
clients/_client_template/overview.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Client Overview
|
||||
|
||||
## Company Name
|
||||
<!-- Replace with company name -->
|
||||
|
||||
## Primary Contact
|
||||
- Name:
|
||||
- Phone:
|
||||
- Email:
|
||||
|
||||
## IT Contact
|
||||
- Name:
|
||||
- Phone:
|
||||
- Email:
|
||||
|
||||
## Contract Details
|
||||
- Service Level:
|
||||
- Hours Covered:
|
||||
- Contract Renewal Date:
|
||||
|
||||
## Environment Summary
|
||||
- Total Users:
|
||||
- Total Locations:
|
||||
- Domain Name:
|
||||
- Primary Site Address:
|
||||
- RMM Agent Count:
|
||||
- Workstation Count:
|
||||
- Server Count:
|
||||
|
||||
## Notes
|
||||
<!-- General notes about this client -->
|
||||
34
clients/_client_template/rmm/rmm.md
Normal file
34
clients/_client_template/rmm/rmm.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# RMM / Monitoring
|
||||
|
||||
## RMM Solution
|
||||
- Product:
|
||||
- Console URL:
|
||||
- Agent Version:
|
||||
|
||||
## Agent Deployment
|
||||
- Total Devices:
|
||||
- Servers Monitored:
|
||||
- Workstations Monitored:
|
||||
- Network Devices Monitored:
|
||||
|
||||
## Monitoring Policies
|
||||
| Policy Name | Applies To | Alert Condition | Action |
|
||||
|-------------------|----------------|-------------------------|---------------|
|
||||
| Disk Space | All Servers | < 10% free | Alert + Ticket|
|
||||
| CPU | All Servers | > 90% for 15 min | Alert |
|
||||
| Service Monitor | All Servers | | |
|
||||
| Backup Monitor | | | |
|
||||
| Offline Alert | All Agents | Offline > 30 min | Alert |
|
||||
|
||||
## Patch Management
|
||||
- Patch Policy:
|
||||
- Patch Window:
|
||||
- Auto-approve: Yes/No
|
||||
- Exclusions:
|
||||
|
||||
## Scripting / Automation
|
||||
| Script Name | Schedule | Purpose |
|
||||
|---------------------|-------------|--------------------------|
|
||||
| | | |
|
||||
|
||||
## Notes
|
||||
26
clients/_client_template/security/antivirus.md
Normal file
26
clients/_client_template/security/antivirus.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Endpoint Security / Antivirus
|
||||
|
||||
## Solution
|
||||
- Product:
|
||||
- Console URL:
|
||||
- License Count:
|
||||
- License Expiry:
|
||||
- Managed By:
|
||||
|
||||
## Policy
|
||||
- Real-time Protection: Yes/No
|
||||
- Scheduled Scans: (frequency)
|
||||
- Exclusions:
|
||||
|
||||
## Deployment Status
|
||||
- Total Endpoints:
|
||||
- Protected:
|
||||
- Missing Agent:
|
||||
- Out of Date:
|
||||
|
||||
## EDR / XDR
|
||||
- EDR Enabled: Yes/No
|
||||
- Product:
|
||||
- Console URL:
|
||||
|
||||
## Notes
|
||||
34
clients/_client_template/security/backup.md
Normal file
34
clients/_client_template/security/backup.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Backup and Disaster Recovery
|
||||
|
||||
## Backup Solution
|
||||
- Product:
|
||||
- Console URL:
|
||||
- License/Subscription:
|
||||
|
||||
## Backup Targets
|
||||
| Target Name | Type | Location | Capacity | Encrypted |
|
||||
|----------------|----------------|-----------------|--------------|-----------|
|
||||
| | Local NAS | | | Yes/No |
|
||||
| | Cloud | | | Yes/No |
|
||||
| | Offsite | | | Yes/No |
|
||||
|
||||
## Backup Jobs
|
||||
| Job Name | Source | Target | Schedule | Retention | Status |
|
||||
|-----------------|-------------------|------------|---------------|-------------|--------|
|
||||
| | | | | | |
|
||||
|
||||
## M365 Backup
|
||||
- M365 Backup Product:
|
||||
- Exchange Backed Up: Yes/No
|
||||
- SharePoint Backed Up: Yes/No
|
||||
- OneDrive Backed Up: Yes/No
|
||||
- Teams Backed Up: Yes/No
|
||||
|
||||
## Disaster Recovery Plan
|
||||
- RTO Target:
|
||||
- RPO Target:
|
||||
- DR Site:
|
||||
- Last DR Test Date:
|
||||
- DR Test Result:
|
||||
|
||||
## Notes
|
||||
49
clients/_client_template/servers/server_template.md
Normal file
49
clients/_client_template/servers/server_template.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Server: [SERVER NAME]
|
||||
|
||||
## General Info
|
||||
- Hostname:
|
||||
- IP Address:
|
||||
- OS:
|
||||
- OS Version:
|
||||
- Physical / Virtual:
|
||||
- Host (if virtual):
|
||||
- Location:
|
||||
- Last Patched:
|
||||
|
||||
## Hardware (if physical)
|
||||
- Make/Model:
|
||||
- CPU:
|
||||
- RAM:
|
||||
- Storage:
|
||||
- Warranty Expiry:
|
||||
|
||||
## Roles and Services
|
||||
<!-- List all roles this server performs -->
|
||||
- [ ] Domain Controller
|
||||
- [ ] DNS Server
|
||||
- [ ] DHCP Server
|
||||
- [ ] File Server
|
||||
- [ ] Print Server
|
||||
- [ ] Application Server
|
||||
- [ ] Database Server
|
||||
- [ ] Backup Target
|
||||
- [ ] RDS / Terminal Server
|
||||
- [ ] Hyper-V Host
|
||||
|
||||
## Shares (if file server)
|
||||
| Share Name | Path | Permissions Group | Notes |
|
||||
|---------------|-------------------|---------------------|----------------|
|
||||
| | | | |
|
||||
|
||||
## Applications Installed
|
||||
| Application | Version | Purpose | License |
|
||||
|-------------------|------------|----------------------|---------------|
|
||||
| | | | |
|
||||
|
||||
## Backup
|
||||
- Backup Method:
|
||||
- Backup Schedule:
|
||||
- Backup Target:
|
||||
- Last Verified Restore:
|
||||
|
||||
## Notes
|
||||
17
clients/ace-portables/PROJECT_STATE.md
Normal file
17
clients/ace-portables/PROJECT_STATE.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Ace Portables — Project State
|
||||
|
||||
> Last updated: 2026-04-20
|
||||
|
||||
**Status:** MAINTENANCE
|
||||
**Last Activity:** Unknown (reports directory only)
|
||||
|
||||
Minimal client presence — directory contains only a `reports/` subdirectory. No session logs, no infrastructure details, no active projects.
|
||||
|
||||
## What Was Done
|
||||
|
||||
- Reports directory created
|
||||
|
||||
## If Resuming
|
||||
|
||||
- Check `clients/ace-portables/reports/` for any generated reports
|
||||
- No infrastructure or contact details recorded — gather before starting any work
|
||||
52
clients/anaise/PROJECT_STATE.md
Normal file
52
clients/anaise/PROJECT_STATE.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Anaise — Project State
|
||||
|
||||
> READ THIS before starting work on this client.
|
||||
> UPDATE THIS when you begin work (claim a lock) and when you finish (release lock + log changes).
|
||||
> Last updated: 2026-04-20
|
||||
|
||||
---
|
||||
|
||||
## Active Session Locks
|
||||
|
||||
| Session | Working On | Status | Started |
|
||||
|---------|-----------|--------|---------|
|
||||
| _(none active)_ | | | |
|
||||
|
||||
**How to claim a lock:** Add a row before starting work. Remove it when done. Locks older than 2 hours with no update are considered stale.
|
||||
|
||||
---
|
||||
|
||||
## Current State
|
||||
|
||||
**Status:** ONBOARDING
|
||||
**Last Activity:** 2026-04-16
|
||||
|
||||
New client. Standard directory template applied 2026-04-16. Onboarding not yet complete. Directory contains only a `docs/` subfolder — minimal information captured.
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure / Access
|
||||
|
||||
No infrastructure details recorded yet. Check `clients/anaise/docs/` for any notes captured during initial onboarding.
|
||||
|
||||
---
|
||||
|
||||
## Pending / Next Up
|
||||
|
||||
- [ ] Complete onboarding — capture infrastructure details, contacts, credentials to vault
|
||||
- [ ] Populate `docs/` with client overview, network diagram, server inventory
|
||||
|
||||
---
|
||||
|
||||
## Recent Changes
|
||||
|
||||
| Date | By | Change | Status |
|
||||
|------|-----|--------|--------|
|
||||
| 2026-04-16 | Howard | Standard client directory structure applied | IN PROGRESS |
|
||||
|
||||
---
|
||||
|
||||
## How to Update
|
||||
|
||||
**When starting:** Add your session to Active Session Locks.
|
||||
**When finishing:** Remove your lock row, add entries to Recent Changes, update Current State if needed.
|
||||
28
clients/anaise/docs/cloud/azure.md
Normal file
28
clients/anaise/docs/cloud/azure.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Azure / Cloud Services
|
||||
|
||||
## Azure Subscription
|
||||
- Subscription Name:
|
||||
- Subscription ID:
|
||||
- Resource Group(s):
|
||||
- Region:
|
||||
- Monthly Spend (approx):
|
||||
|
||||
## Virtual Machines
|
||||
| VM Name | Size | OS | IP | Purpose |
|
||||
|---------------|------------|------------|------------|-----------------|
|
||||
| | | | | |
|
||||
|
||||
## Networking
|
||||
- Virtual Network:
|
||||
- Address Space:
|
||||
- Subnets:
|
||||
- VPN Gateway to On-Prem: Yes/No
|
||||
- ExpressRoute: Yes/No
|
||||
|
||||
## Other Cloud Services
|
||||
<!-- AWS, Google Workspace, third-party SaaS -->
|
||||
| Service | Purpose | Admin URL | Notes |
|
||||
|-----------------|------------------|------------------|-----------------|
|
||||
| | | | |
|
||||
|
||||
## Notes
|
||||
52
clients/anaise/docs/cloud/m365.md
Normal file
52
clients/anaise/docs/cloud/m365.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Microsoft 365
|
||||
|
||||
## Tenant Info
|
||||
- Tenant Name:
|
||||
- Tenant ID:
|
||||
- Primary Domain:
|
||||
- Admin Portal URL: https://admin.microsoft.com
|
||||
|
||||
## Licensing
|
||||
| License Type | Quantity | Assigned | Available |
|
||||
|--------------------------|----------|----------|-----------|
|
||||
| Microsoft 365 Business Basic | | | |
|
||||
| Microsoft 365 Business Standard | | | |
|
||||
| Microsoft 365 Business Premium | | | |
|
||||
| Exchange Online Plan 1/2 | | | |
|
||||
| Other | | | |
|
||||
|
||||
## Exchange Online
|
||||
- Mail Domain(s):
|
||||
- MX Record Points To:
|
||||
- SPF Record:
|
||||
- DKIM Enabled: Yes/No
|
||||
- DMARC Policy:
|
||||
- Shared Mailboxes:
|
||||
- Distribution Groups:
|
||||
- Mail Flow Rules: Yes/No (describe below)
|
||||
|
||||
## SharePoint / OneDrive
|
||||
- SharePoint Sites:
|
||||
- External Sharing: Enabled/Disabled
|
||||
- OneDrive Storage Limit:
|
||||
|
||||
## Teams
|
||||
- Teams Phone System: Yes/No
|
||||
- Calling Plan / Direct Routing:
|
||||
- Auto Attendant:
|
||||
|
||||
## Entra ID (Azure AD)
|
||||
- Hybrid Joined: Yes/No
|
||||
- Azure AD Connect Server:
|
||||
- Sync Schedule:
|
||||
- Password Hash Sync: Yes/No
|
||||
- MFA Enforced: Yes/No
|
||||
- Conditional Access Policies:
|
||||
|
||||
## Security
|
||||
- Defender for Office 365: Yes/No
|
||||
- Safe Links: Yes/No
|
||||
- Safe Attachments: Yes/No
|
||||
- Audit Log Retention:
|
||||
|
||||
## Notes
|
||||
19
clients/anaise/docs/issues/log.md
Normal file
19
clients/anaise/docs/issues/log.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Issue Log
|
||||
|
||||
Record past issues and their resolutions here. This helps the AI learn from historical
|
||||
troubleshooting and avoid repeating failed approaches.
|
||||
|
||||
## Template
|
||||
|
||||
### [DATE] - [Brief Description]
|
||||
- **Reported By:**
|
||||
- **Severity:** Low / Medium / High / Critical
|
||||
- **Symptoms:**
|
||||
- **Root Cause:**
|
||||
- **Resolution:**
|
||||
- **Time to Resolve:**
|
||||
- **Lessons Learned:**
|
||||
|
||||
---
|
||||
|
||||
<!-- Add new issues above this line, newest first -->
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user