# Security & CX Audit Report

**Date:** 2026-02-11
**Scope:** Property Mapping & RFM Thresholds Implementation
**Status:** 🔍 In Progress

## Executive Summary

This audit identifies potential security vulnerabilities and customer experience issues in the newly implemented configuration features.

## 🔒 SECURITY ISSUES

### 1. ❌ CRITICAL: Path Traversal Vulnerability

**Location:** `server.py:48-61`

**Issue:**
```python
mapping_path = os.getenv("ARTEFACT_PROPERTY_MAPPING_PATH")
if mapping_path and os.path.exists(mapping_path):
    with open(mapping_path, "r") as f:
        mapping_config = json.load(f)
```

**Risk:** User-controlled file path without validation. Attacker could set env var to read arbitrary files.

**Attack Vector:**
```bash
export ARTEFACT_PROPERTY_MAPPING_PATH=/etc/passwd
```

**Severity:** HIGH
**Impact:** Arbitrary file read

**Fix:** Validate file path and restrict to safe directories.

---

### 2. ❌ HIGH: JSON Deserialization Without Schema Validation

**Location:** `server.py:54`, `server.py:71`

**Issue:**
```python
mapping_config = json.load(f)
_property_mapping = PropertyMapping(**mapping_config)
```

**Risk:** Malformed JSON or unexpected fields could cause crashes or unexpected behavior.

**Attack Vector:**
```json
{
  "tech_stack": {"__class__": "malicious"},
  "growth_signals": [1, 2, 3, ...repeat 100000 times]
}
```

**Severity:** MEDIUM
**Impact:** DoS, application crash, potential injection if dataclass doesn't validate

**Fix:** Validate JSON structure before deserialization.

---

### 3. ❌ MEDIUM: No Maximum File Size Check

**Location:** `server.py:54`, `server.py:71`

**Issue:** No file size limit when reading configuration files.

**Attack Vector:**
```bash
# Create 1GB JSON file
export ARTEFACT_PROPERTY_MAPPING_PATH=/path/to/huge_file.json
```

**Severity:** MEDIUM
**Impact:** Memory exhaustion, DoS

**Fix:** Check file size before reading.

---

### 4. ⚠️ LOW: Error Messages May Leak File System Information

**Location:** `server.py:58-61`

**Issue:**
```python
except Exception as e:
    print(f"[Artefact MCP] Failed to load property mapping: {e}", file=sys.stderr)
```

**Risk:** Stack traces or detailed error messages could reveal file paths or internal structure.

**Severity:** LOW
**Impact:** Information disclosure

**Fix:** Sanitize error messages.

---

### 5. ⚠️ INFO: HubSpot Property Names Not Sanitized

**Location:** `property_mapper.py:16-49`

**Issue:** Property names from JSON are directly used in HubSpot API calls without validation.

**Risk:** If attacker controls property mapping, could attempt to access unintended HubSpot properties.

**Severity:** LOW (requires HubSpot API key already compromised)
**Impact:** Potential data access beyond intended scope

**Fix:** Validate property names against allowed patterns.

---

## 😟 CUSTOMER EXPERIENCE ISSUES

### 1. ❌ CRITICAL: Silent Failures on Invalid Configuration

**Location:** `server.py:58-61`, `server.py:75-78`

**Issue:**
```python
except Exception as e:
    print(f"[Artefact MCP] Failed to load property mapping: {e}", file=sys.stderr)
    print("[Artefact MCP] Using default property mapping.", file=sys.stderr)
    _property_mapping = DEFAULT_PROPERTY_MAPPING
```

**Problem:** User thinks they configured custom mapping, but defaults are used silently.

**Impact:**
- Wrong scores returned
- User doesn't realize configuration failed
- No clear error in MCP tool response

**Fix:** Return validation errors to user through MCP tool response.

---

### 2. ❌ HIGH: No Configuration Validation Feedback

**Location:** `property_mappings.py`, `rfm_thresholds.py`

**Issue:** No validation that:
- HubSpot properties exist
- Thresholds are in logical order
- Arrays have matching lengths
- Percentiles sum to reasonable values

**Examples of Bad Configs That Will Silently Fail:**
```json
{
  "recency_days": [365, 180, 90, 30],  // Backwards!
  "recency_scores": [1, 2, 3, 4, 5]
}
```

```json
{
  "growth_signals": ["nonexistent_property"],  // Will never match
  "content_engagement": "typo_property_name"   // Will default to "none"
}
```

**Fix:** Add validation methods to dataclasses.

---

### 3. ❌ HIGH: Poor Error Context When HubSpot Fetch Fails

**Location:** `hubspot_client.py:216-238`

**Issue:** Generic HubSpot errors don't indicate which custom property caused failure.

**Example:**
```
HubSpot API error (400): Property 'tech_stack_typo' does not exist
```

User has no idea this came from their property mapping config.

**Fix:** Wrap HubSpot calls with context about which property failed.

---

### 4. ⚠️ MEDIUM: No Configuration Test/Validation Tool

**Issue:** Users must run full analysis to discover config errors.

**Problem:**
- Long feedback loop
- Wastes API calls
- Hard to debug

**Fix:** Add validation tool or endpoint.

---

### 5. ⚠️ MEDIUM: Confusing Behavior When Both Config File AND Preset Used

**Location:** `rfm.py:41-61`

**Issue:**
```python
def _get_scorer(industry_preset: str, custom_thresholds: Optional[RFMThresholds] = None):
    if custom_thresholds:
        return RFMScorer(thresholds=custom_thresholds.to_scorer_config())
    # Falls through to preset
```

**Problem:** User passes `industry_preset="saas"` AND sets `ARTEFACT_RFM_THRESHOLDS_PATH`. Which wins?

**Current behavior:** Custom file wins (not documented clearly).

**Fix:** Document priority order clearly, warn user if both are set.

---

### 6. ⚠️ MEDIUM: No Dry-Run Mode

**Issue:** Users can't test configuration without hitting HubSpot API.

**Problem:**
- Can't validate config during setup
- Risk of hitting rate limits during testing
- No way to see what properties would be fetched

**Fix:** Add `dry_run=true` parameter to tools.

---

### 7. ⚠️ LOW: Example Files Have No Validation Comments

**Location:** `property_mapping.example.json`, `rfm_thresholds.example.json`

**Issue:** JSON files have no inline documentation about constraints.

**Example of what's missing:**
```json
{
  "recency_days": [30, 90, 180, 365],  // Must be ascending order!
  "recency_scores": [5, 4, 3, 2, 1],   // Must be descending order!
  // Arrays must have same length
}
```

**Fix:** Add JSON Schema or comments.

---

### 8. ⚠️ LOW: No Warning When Deal Count Fetch is Slow

**Location:** `icp.py:94-98`

**Issue:**
```python
def _fetch_deal_count(client: HubSpotClient, company_id: str) -> int:
    deals = client.fetch_client_data(stage_filter="closedwon", limit=1000)
    count = sum(1 for d in deals if d.get("client_id") == company_id)
    return count
```

**Problem:** Fetches up to 1000 deals for EVERY company qualification. Very slow!

**Impact:**
- User thinks tool is hanging
- Excessive API calls
- Poor performance

**Fix:** Cache deal data or fetch only for specific company.

---

## 📋 RECOMMENDED FIXES

### Priority 1: Security Fixes (Implement Immediately)

**1. Add Path Validation**
```python
def _validate_config_path(path: str) -> bool:
    """Validate configuration file path is safe."""
    # Must be absolute path
    if not os.path.isabs(path):
        return False

    # Must be .json file
    if not path.endswith('.json'):
        return False

    # Resolve symlinks and check it's a file
    real_path = os.path.realpath(path)
    if not os.path.isfile(real_path):
        return False

    # Check file size (max 1MB)
    if os.path.getsize(real_path) > 1_000_000:
        return False

    return True
```

**2. Add JSON Schema Validation**
```python
PROPERTY_MAPPING_SCHEMA = {
    "type": "object",
    "properties": {
        "tech_stack": {"type": ["string", "null"]},
        "growth_signals": {
            "type": ["array", "null"],
            "items": {"type": "string"},
            "maxItems": 50
        },
        # ... etc
    },
    "additionalProperties": False
}

# Validate before creating PropertyMapping
jsonschema.validate(mapping_config, PROPERTY_MAPPING_SCHEMA)
```

**3. Add File Size Check**
```python
if os.path.getsize(mapping_path) > 1_000_000:  # 1MB max
    raise ValueError("Configuration file too large (max 1MB)")
```

---

### Priority 2: Critical CX Fixes

**1. Add Configuration Validation to Dataclasses**
```python
@dataclass
class RFMThresholds:
    # ... fields ...

    def __post_init__(self):
        """Validate configuration after initialization."""
        self.validate()

    def validate(self):
        """Validate thresholds are logically consistent."""
        # Check recency days are ascending
        if self.recency_days != sorted(self.recency_days):
            raise ValueError("recency_days must be in ascending order")

        # Check recency scores are descending
        if self.recency_scores != sorted(self.recency_scores, reverse=True):
            raise ValueError("recency_scores must be in descending order")

        # Check array lengths match
        if len(self.recency_days) + 1 != len(self.recency_scores):
            raise ValueError("recency_scores must have one more element than recency_days")

        # ... etc
```

**2. Return Validation Errors Through MCP Tools**
```python
@mcp.tool()
def qualify(company_id: Optional[str] = None, company_data: Optional[str] = None) -> str:
    # ... existing code ...

    # Add validation result to response
    validation_warnings = []

    if _property_mapping:
        warnings = _validate_property_mapping(_property_mapping, client)
        validation_warnings.extend(warnings)

    return json.dumps({
        **result,
        "config_validation": {
            "warnings": validation_warnings,
            "config_source": "custom" if _property_mapping else "default"
        }
    }, indent=2, default=str)
```

**3. Add Configuration Test Tool**
```python
@mcp.tool()
def validate_config() -> str:
    """Validate property mapping and RFM threshold configurations.

    Returns validation results without making API calls.
    """
    results = {
        "property_mapping": None,
        "rfm_thresholds": None,
        "errors": [],
        "warnings": []
    }

    # Validate property mapping
    if _property_mapping:
        try:
            _property_mapping.validate()
            results["property_mapping"] = "valid"
        except Exception as e:
            results["errors"].append(f"Property mapping: {e}")

    # Validate RFM thresholds
    if _rfm_thresholds:
        try:
            _rfm_thresholds.validate()
            results["rfm_thresholds"] = "valid"
        except Exception as e:
            results["errors"].append(f"RFM thresholds: {e}")

    return json.dumps(results, indent=2)
```

---

### Priority 3: Performance & UX Improvements

**1. Fix Slow Deal Count Fetch**
```python
def _fetch_deal_count(client: HubSpotClient, company_id: str) -> int:
    """Fetch closed-won deal count for a specific company."""
    # Use HubSpot associations API instead of fetching all deals
    url = f"{client.BASE_URL}/crm/v3/objects/companies/{company_id}/associations/deals"
    response = client._request("GET", url)

    # Filter for closed-won deals
    deal_ids = [a["id"] for a in response.json().get("results", [])]

    if not deal_ids:
        return 0

    # Batch fetch deal stages
    # ... more efficient implementation
```

**2. Add Dry-Run Mode**
```python
@mcp.tool()
def qualify(
    company_id: Optional[str] = None,
    company_data: Optional[str] = None,
    dry_run: bool = False
) -> str:
    """Score a prospect using the ICP Triangulation Framework.

    Args:
        dry_run: If true, shows what would be fetched without making API calls.
    """
    if dry_run:
        property_names = _get_required_properties(_property_mapping)
        return json.dumps({
            "dry_run": True,
            "properties_to_fetch": property_names,
            "config_source": "custom" if _property_mapping else "default"
        }, indent=2)

    # ... normal execution
```

---

## 📊 RISK ASSESSMENT

| Issue | Severity | Likelihood | Impact | Priority |
|-------|----------|------------|--------|----------|
| Path traversal | HIGH | MEDIUM | File read | P1 |
| JSON injection | MEDIUM | MEDIUM | DoS/crash | P1 |
| File size DoS | MEDIUM | LOW | Memory exhaustion | P1 |
| Silent config failures | HIGH | HIGH | Wrong results | P2 |
| No validation feedback | HIGH | HIGH | Confusion | P2 |
| Slow deal fetch | MEDIUM | HIGH | Poor UX | P3 |
| No dry-run mode | MEDIUM | MEDIUM | Testing friction | P3 |

---

## ✅ MITIGATION CHECKLIST

### Must Fix Before Production (P1)
- [ ] Add path validation with whitelist/blacklist
- [ ] Add file size checks (1MB max)
- [ ] Add JSON schema validation
- [ ] Sanitize error messages (no file paths in output)
- [ ] Add try-except around all config parsing

### Should Fix Soon (P2)
- [ ] Add `__post_init__` validation to dataclasses
- [ ] Return config validation warnings in tool responses
- [ ] Add `validate_config` tool
- [ ] Document priority order for config vs presets
- [ ] Add better error context for HubSpot failures

### Nice to Have (P3)
- [ ] Add dry-run mode to tools
- [ ] Optimize deal count fetch (use associations API)
- [ ] Add JSON Schema files for configs
- [ ] Add inline comments to example configs
- [ ] Add configuration migration/upgrade tool

---

## 🎯 NEXT STEPS

1. **Immediate:** Implement P1 security fixes
2. **This week:** Implement P2 CX fixes
3. **Next sprint:** Implement P3 improvements
4. **Document:** Update README with security best practices

