diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 076e407..0000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,30 +0,0 @@ -# Code Owners for XARF JavaScript Library -# These owners will be automatically requested for review when someone opens a pull request. - -# Default owners for everything in the repo -* @xarf/maintainers - -# Core library files -/src/** @xarf/maintainers -/tests/** @xarf/maintainers - -# Security-sensitive files (require extra scrutiny) -/src/parser.ts @xarf/maintainers @xarf/security-team -/src/validator.ts @xarf/maintainers @xarf/security-team -/src/generator.ts @xarf/maintainers @xarf/security-team - -# Security and compliance -/.github/workflows/ @xarf/maintainers @xarf/devops-team -/.github/SECURITY.md @xarf/maintainers @xarf/security-team -/.github/dependabot.yml @xarf/maintainers @xarf/devops-team - -# Documentation -/docs/** @xarf/maintainers @xarf/docs-team -README.md @xarf/maintainers @xarf/docs-team -CONTRIBUTING.md @xarf/maintainers - -# Build and config files -package.json @xarf/maintainers -package-lock.json @xarf/maintainers -tsconfig.json @xarf/maintainers -.eslintrc.json @xarf/maintainers diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 62e80dc..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,17 +0,0 @@ -# Funding information for XARF JavaScript Library -# These funding platforms will be displayed on the repository sidebar - -# GitHub Sponsors -github: [xarf] - -# Open Collective -# open_collective: xarf - -# Ko-fi -# ko_fi: xarf - -# Patreon -# patreon: xarf - -# Custom URLs -custom: ['https://xarf.org/support', 'https://xarf.org/donate'] diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 54a0fba..bab8fe0 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -1,116 +1,57 @@ # Security Policy -## Supported Versions - -| Version | Supported | -| ------- | ------------------ | -| 1.0.0 | :white_check_mark: | -| 1.0.0-alpha.2 | :x: (upgrade to 1.0.0) | -| 1.0.0-alpha.1 | :x: (upgrade to 1.0.0) | - ## Reporting a Vulnerability -We take security vulnerabilities seriously. If you discover a security issue in this project, please report it responsibly. +The XARF project takes security vulnerabilities seriously. We appreciate your efforts to responsibly disclose your findings. ### How to Report -**DO NOT** open a public GitHub issue for security vulnerabilities. - -Instead, please email security details to: **security@xarf.org** - -Include the following information in your report: -- Description of the vulnerability -- Steps to reproduce the issue -- Potential impact -- Suggested fix (if available) - -### What to Expect - -- **Acknowledgment**: We will acknowledge receipt of your vulnerability report within 48 hours -- **Assessment**: We will assess the severity and impact of the vulnerability -- **Updates**: We will keep you informed of our progress toward a fix -- **Disclosure**: Once a fix is available, we will coordinate disclosure timing with you - -## Security Best Practices - -When using the XARF JavaScript parser, follow these security best practices: - -### Input Validation - -1. **Always validate XARF reports** against the schema before processing -2. **Sanitize all user-supplied data** before using it in XARF reports -3. **Set size limits** on incoming reports to prevent memory exhaustion -4. **Validate email addresses** and other contact information before use - -### Safe Parsing - -```javascript -// Example: Safe parsing with error handling -try { - const report = parser.parse(input); - - // Validate against schema - if (!validator.validate(report)) { - throw new Error('Invalid XARF report structure'); - } - - // Process validated report - processReport(report); -} catch (error) { - // Handle parsing errors securely - logger.error('Parsing failed', { error: error.message }); - // Do not expose internal details to users -} -``` +**Please DO NOT report security vulnerabilities through public GitHub issues.** -### Data Handling +Instead, please report security vulnerabilities by emailing: -1. **Do not log sensitive information** from XARF reports -2. **Redact PII** when logging or storing reports -3. **Use secure transport** (HTTPS/TLS) when transmitting reports -4. **Encrypt sensitive data** at rest +**security@abusix.com** -### Dependency Management +### What to Include -1. **Regularly update dependencies** to patch known vulnerabilities -2. **Use `npm audit`** to check for security issues -3. **Review security advisories** for dependencies -4. **Consider using lock files** (`package-lock.json`) for reproducible builds +Please include the following information in your report: -### Code Practices +- Type of vulnerability or security concern +- Affected specification version(s) +- Detailed description of the security issue +- Potential impact on implementations +- Suggested mitigation or fix (if applicable) -1. **Avoid eval()** and similar dynamic code execution -2. **Use strict mode** (`"use strict"`) -3. **Validate all inputs** before processing -4. **Follow principle of least privilege** in code design +### Response Timeline -## Known Security Considerations +- **Initial Response**: Within 48 hours +- **Status Update**: Within 7 days +- **Resolution**: Depends on severity and complexity -### XARF Report Content +### Security Update Process -XARF reports may contain: -- Email addresses and contact information -- IP addresses and network data -- Potentially malicious content samples -- Sensitive abuse details +1. **Triage**: We'll confirm the vulnerability and assess severity +2. **Specification Review**: We'll review affected specification sections +3. **Fix Development**: We'll develop and review proposed changes +4. **Community Review**: We'll engage with implementation maintainers +5. **Disclosure**: We'll coordinate disclosure timing with you +6. **Publication**: We'll publish updated specification with security notes -**Always treat XARF report content as untrusted user input.** +## Vulnerability Disclosure Policy -### Schema Validation +We follow a **coordinated disclosure** model: -While the parser validates structure, additional application-level validation may be required for: -- Email address format verification -- IP address range validation -- URL safety checks -- Content length restrictions +1. **Private Disclosure**: Report sent to security@abusix.com +2. **Acknowledgment**: We confirm receipt within 48 hours +3. **Investigation**: We investigate with specification experts +4. **Community Review**: We consult with implementation maintainers +5. **Specification Update**: We publish updated specification +6. **Public Disclosure**: We publish advisory 7 days after publication -## Security Updates +## Security Hall of Fame -Security updates will be released as soon as possible after a vulnerability is confirmed and fixed. Updates will be announced through: -- GitHub Security Advisories -- Release notes -- Project changelog +We recognize security researchers who responsibly disclose vulnerabilities: -## Acknowledgments + -We appreciate the security research community's efforts in responsibly disclosing vulnerabilities. Contributors who report valid security issues will be acknowledged (with their permission) in our security advisories. +_No vulnerabilities reported yet._ diff --git a/.github/SECURITY_IMPROVEMENTS.md b/.github/SECURITY_IMPROVEMENTS.md deleted file mode 100644 index 1d3c2b0..0000000 --- a/.github/SECURITY_IMPROVEMENTS.md +++ /dev/null @@ -1,393 +0,0 @@ -# Security Configuration Improvements Summary - -This document summarizes all security improvements made to the XARF JavaScript library repository. - -## Overview - -Comprehensive security hardening has been implemented across GitHub workflows, dependency management, and security monitoring. - -## Changes Made - -### 1. Enhanced CodeQL Analysis - -**File:** `.github/workflows/codeql.yml` - -**Improvements:** -- ✅ Increased scan frequency to twice weekly (Monday/Thursday) -- ✅ Added `workflow_dispatch` for manual triggering -- ✅ Added full history checkout (`fetch-depth: 0`) for better analysis -- ✅ Added Node.js setup and dependency installation for accurate analysis -- ✅ Configured path exclusions (node_modules, dist, coverage, tests) -- ✅ Added SARIF output upload for advanced review -- ✅ Added pull-requests read permission - -**Benefits:** -- More frequent security scans catch issues faster -- Better code analysis with full project context -- Advanced reporting via SARIF format - ---- - -### 2. Improved Dependabot Configuration - -**File:** `.github/dependabot.yml` - -**Improvements:** -- ✅ Changed npm checks from weekly to **daily** for faster security patches -- ✅ Increased pull request limit from 10 to 15 -- ✅ Added assignees for dependency PRs -- ✅ Added `rebase-strategy: auto` for cleaner history -- ✅ Improved grouping strategy: - - Separate groups for dev dependency patches and minors - - Production patches kept separate for careful review - - Major updates always separate -- ✅ Added vulnerability-alerts labels configuration -- ✅ Added GitHub Actions grouping for minor/patch updates -- ✅ Comprehensive comments explaining configuration - -**Benefits:** -- Faster response to security vulnerabilities -- Better organization of dependency updates -- Cleaner PR history with automatic rebasing -- Clear separation between production and development updates - ---- - -### 3. Enhanced Dependency Review - -**File:** `.github/workflows/dependency-review.yml` - -**Improvements:** -- ✅ Added `workflow_dispatch` for manual triggering -- ✅ Added `issues: write` permission -- ✅ Enhanced license blocking (GPL-2.0, LGPL variants) -- ✅ Explicit allow-list for permissive licenses -- ✅ Changed PR comments to `always` (not just on failure) -- ✅ Enabled vulnerability checking -- ✅ Enabled license checking -- ✅ Added dependency graph artifact upload (30-day retention) -- ✅ Added explicit base-ref and head-ref configuration - -**Benefits:** -- Comprehensive license compliance enforcement -- Always-visible dependency changes in PRs -- Historical tracking of dependency reviews -- Better PR review context - ---- - -### 4. New: OpenSSF Scorecard Workflow - -**File:** `.github/workflows/scorecard.yml` (NEW) - -**Features:** -- ✅ Weekly automated security score assessment -- ✅ Manual trigger capability -- ✅ Runs on main branch pushes -- ✅ Uploads results to Security tab -- ✅ Stores SARIF artifacts (5-day retention) -- ✅ Minimal permissions (read-all by default) -- ✅ Proper permissions for uploads (security-events: write) - -**Benefits:** -- Objective security posture measurement -- Tracks security best practices compliance -- Identifies improvement opportunities -- Industry-standard security scoring - ---- - -### 5. New: Comprehensive Security Scanning - -**File:** `.github/workflows/security-scan.yml` (NEW) - -**Features:** -- ✅ Daily automated security scans (2 AM UTC) -- ✅ Manual trigger and PR scanning -- ✅ Four security jobs: - 1. **Secret Scanning** (TruffleHog) - Detects secrets in code - 2. **OWASP Dependency Check** - Comprehensive vulnerability scanning - 3. **NPM Audit** - npm-specific vulnerability checks - 4. **License Compliance** - Automated license verification -- ✅ Security summary job - Aggregates all scan results -- ✅ Artifacts uploaded with 30-day retention -- ✅ Strict license compliance enforcement - -**Benefits:** -- Multi-layered security scanning -- Early detection of secrets before commit -- Comprehensive vulnerability database checks -- Automated license compliance verification -- Historical audit trail via artifacts - ---- - -### 6. Enhanced CI Security Checks - -**File:** `.github/workflows/ci.yml` - -**Improvements:** -- ✅ Separated npm audit for all deps vs production-only -- ✅ Added outdated dependency checks -- ✅ Added package-lock.json integrity verification -- ✅ Added npm-audit-resolver integration (when available) -- ✅ Added license compliance check via license-checker -- ✅ Better error handling with continue-on-error - -**Benefits:** -- Stricter production dependency security -- Detect package-lock tampering -- Proactive outdated dependency awareness -- License compliance in CI pipeline - ---- - -### 7. Repository Organization - -**Changes:** -- ✅ Moved `SECURITY.md` to `.github/SECURITY.md` (GitHub standard) -- ✅ Created `.github/CODEOWNERS` for required reviewers -- ✅ Created `.github/FUNDING.yml` for sponsorship -- ✅ Created security issue template (`.github/ISSUE_TEMPLATE/security_vulnerability.yml`) -- ✅ Created comprehensive security recommendations (`.github/security-recommendations.md`) - -**Benefits:** -- Standard GitHub security documentation location -- Automatic review assignment for sensitive files -- Structured security vulnerability reporting -- Comprehensive security guidance for maintainers - ---- - -### 8. CODEOWNERS Configuration - -**File:** `.github/CODEOWNERS` (NEW) - -**Coverage:** -- ✅ Default: All files → `@xarf/maintainers` -- ✅ Core library: `src/**`, `tests/**` → `@xarf/maintainers` -- ✅ Security-sensitive: Parser, validator, generator → `@xarf/maintainers` + `@xarf/security-team` -- ✅ Infrastructure: Workflows → `@xarf/maintainers` + `@xarf/devops-team` -- ✅ Security files: SECURITY.md, dependabot.yml → Security team required -- ✅ Documentation: docs, README → `@xarf/docs-team` -- ✅ Build files: package.json, tsconfig.json → Maintainers - -**Benefits:** -- Automatic expert review assignment -- Required security team approval for sensitive code -- Clear ownership and accountability -- Faster, more thorough reviews - ---- - -### 9. Security Issue Template - -**File:** `.github/ISSUE_TEMPLATE/security_vulnerability.yml` (NEW) - -**Features:** -- ✅ Structured security vulnerability reporting -- ✅ Severity assessment checkboxes -- ✅ Required fields: description, impact, reproduction -- ✅ Optional fields: affected versions, environment, suggested fix -- ✅ Security checklist for reporters -- ✅ Clear guidance to use private reporting for serious issues -- ✅ Auto-labels: `security`, `triage` -- ✅ Auto-assigns: `@xarf/security-team` - -**Benefits:** -- Consistent security reports -- Faster triage with structured information -- Encourages responsible disclosure -- Clear severity assessment - ---- - -### 10. Security Recommendations Documentation - -**File:** `.github/security-recommendations.md` (NEW) - -**Contents:** -- ✅ Repository security best practices -- ✅ Branch protection recommendations -- ✅ Dependency management guidelines -- ✅ Secure coding practices -- ✅ CI/CD security guidance -- ✅ Vulnerability response process -- ✅ Security monitoring procedures -- ✅ Severity levels and response times -- ✅ Metrics to track -- ✅ External resources - -**Benefits:** -- Comprehensive security playbook -- Clear processes for security incidents -- Onboarding guide for security practices -- Reference documentation for maintainers - ---- - -## Security Workflow Matrix - -| Workflow | Frequency | Trigger | Purpose | -|----------|-----------|---------|---------| -| CodeQL | 2x weekly, push, PR | Scheduled, push, PR | Code security analysis | -| Dependency Review | PR only | Pull requests | PR dependency safety | -| Security Scan | Daily, push, PR | Scheduled, push, PR | Multi-layered scanning | -| OSSF Scorecard | Weekly, push | Scheduled, push | Security posture scoring | -| CI (Security Job) | Push, PR | Push, PR | Continuous security checks | - ---- - -## Before & After Comparison - -### Before -- ✅ CodeQL weekly -- ✅ Basic Dependabot (weekly) -- ✅ Basic dependency review -- ✅ Simple npm audit in CI -- ❌ No secret scanning -- ❌ No OSSF Scorecard -- ❌ No comprehensive security scanning -- ❌ No CODEOWNERS -- ❌ No security issue template - -### After -- ✅ CodeQL **twice weekly** with enhanced configuration -- ✅ Dependabot **daily** with intelligent grouping -- ✅ Enhanced dependency review with license checks -- ✅ Comprehensive npm audit with integrity checks -- ✅ **NEW**: TruffleHog secret scanning -- ✅ **NEW**: OSSF Scorecard -- ✅ **NEW**: Multi-layered security scanning workflow -- ✅ **NEW**: CODEOWNERS with security team -- ✅ **NEW**: Security issue template -- ✅ **NEW**: Security recommendations documentation -- ✅ **NEW**: License compliance automation - ---- - -## Recommended Next Steps - -### Immediate (Repository Settings) - -1. **Enable GitHub Security Features** - ``` - Repository Settings → Security → Enable: - - Dependabot security updates ✓ - - Secret scanning ✓ - - Push protection ✓ - ``` - -2. **Configure Branch Protection** - ``` - Settings → Branches → Branch protection rules for 'main': - - Require pull request reviews (2 approvals) - - Require status checks - - Require signed commits - - Include administrators - ``` - -3. **Create GitHub Teams** (if not exist) - ``` - - @xarf/maintainers - - @xarf/security-team - - @xarf/devops-team - - @xarf/docs-team - ``` - -### Short-term (1-2 weeks) - -4. **Review and Fix Security Findings** - - Check CodeQL alerts - - Review Dependabot alerts - - Address OSSF Scorecard recommendations - -5. **Test Workflows** - - Verify all workflows run successfully - - Test manual workflow triggers - - Review artifact uploads - -6. **Documentation** - - Update README with security badges - - Link to SECURITY.md in README - - Create CONTRIBUTING.md security section - -### Long-term (Ongoing) - -7. **Regular Reviews** - - Monthly: Review open security alerts - - Quarterly: Comprehensive security audit - - Annually: External security assessment - -8. **Metrics Dashboard** - - Track MTTD (Mean Time To Detect) - - Track MTTR (Mean Time To Resolve) - - Monitor OSSF Scorecard trend - -9. **Security Training** - - Onboard team on security processes - - Regular security awareness training - - Incident response drills - ---- - -## Success Metrics - -### Security Posture Improvements - -| Metric | Before | Target | -|--------|--------|--------| -| Security scans/week | 1 (CodeQL) | 10+ (multiple workflows) | -| Dependency checks | Weekly | Daily | -| Secret scanning | None | Automated | -| License compliance | Manual | Automated | -| Security issue template | No | Yes | -| CODEOWNERS | No | Yes | -| OSSF Scorecard | No | Yes | -| Response time | Undefined | Defined by severity | - -### Coverage - -- ✅ 100% of security-sensitive files require security team review -- ✅ 100% of pull requests get dependency review -- ✅ 100% of commits scanned for secrets -- ✅ 100% of dependencies checked for vulnerabilities -- ✅ 100% of licenses validated - ---- - -## Maintenance - -### Daily -- Dependabot checks run automatically -- Security scan workflow runs automatically - -### Weekly -- Review Dependabot PRs -- Check OSSF Scorecard results -- Review CodeQL findings - -### Monthly -- Review all open security alerts -- Update security documentation -- Check workflow metrics - -### Quarterly -- Comprehensive security audit -- Review and update security policies -- Test incident response - ---- - -## Support - -For questions about these security improvements: -- Email: security@xarf.org -- GitHub Security: [Security Tab](https://github.com/xarf/xarf-javascript/security) -- Documentation: `.github/security-recommendations.md` - ---- - -**Document Version:** 1.0.0 -**Last Updated:** 2025-12-16 -**Author:** Security Team diff --git a/.github/SENIOR_ENGINEER_FIXES.md b/.github/SENIOR_ENGINEER_FIXES.md deleted file mode 100644 index ff81a78..0000000 --- a/.github/SENIOR_ENGINEER_FIXES.md +++ /dev/null @@ -1,230 +0,0 @@ -# Senior Engineer Feedback - All Issues Resolved ✅ - -**Status**: All 5 issues fixed and verified -**Commit**: `87d3514` - fix: enhance validation and developer feedback per senior engineer review -**Tests**: 10/10 passing in `tests/senior-engineer-feedback.test.ts` - ---- - -## Issue 1: Snake case vs CamelCase Support ✅ - -**Problem**: XARF spec uses snake_case, but library didn't work with spec examples - -**Solution**: Library already supports snake_case natively -- Verified with direct XARF v4 spec examples -- All snake_case properties work correctly -- No changes needed - working as expected - -**Test Results**: ✅ 2/2 tests passing -```typescript -// Works correctly with XARF spec examples -{ - xarf_version: '4.0.0', - report_id: 'test-123', - source_identifier: '192.0.2.100', - evidence_source: 'honeypot' - // All snake_case properties accepted -} -``` - ---- - -## Issue 2: Invalid Properties Emit Warnings ✅ - -**Problem**: Typos and misspelled properties silently ignored - -**Solution**: Added `checkForUnknownProperties()` method -- **File**: `src/parser.ts:211-293` -- Validates all properties against known XARF fields -- Emits warnings for unknown/misspelled properties -- Warnings accessible via `parser.getWarnings()` - -**Test Results**: ✅ 2/2 tests passing -```typescript -// Now warns about typos -{ severety: 'high' } // Warning: "Unknown property 'severety'" -{ sourcePort: 443 } // Warning: "Unknown property 'sourcePort'" -{ contentType: '...' } // Warning: "Unknown property 'contentType'" -``` - -**Known Fields**: -- **Base**: xarf_version, report_id, timestamp, reporter, sender, source_identifier, category, type, evidence_source, on_behalf_of, description, evidence, tags, severity, confidence, occurrence, target -- **Messaging**: protocol, smtp_from, smtp_to, subject, message_id, sender_display_name, target_victim, message_content -- **Connection**: destination_ip, destination_port, source_port, attack_type, duration_minutes, packet_count, byte_count, attempt_count, successful_logins, usernames_attempted, attack_pattern -- **Content**: url, content_type, affected_pages, cms_platform, vulnerability_exploited, affected_parameters, payload_detected, data_exposed, database_type, records_potentially_affected -- **Infrastructure**: infrastructure_type, affected_services -- **Copyright**: copyright_holder, infringing_content, original_content -- **Vulnerability**: cve_id, vulnerability_type, affected_software, affected_version -- **Reputation**: reputation_score, blocklists - ---- - -## Issue 3: ReportType Alias ✅ - -**Problem**: Should accept both `type` and `ReportType` - -**Solution**: Already working - both variants accepted -- TypeScript interfaces support both -- Parser handles both field names -- No changes needed - -**Test Results**: ✅ 1/1 tests passing -```typescript -// Both work -{ type: 'ddos' } // ✅ Works -{ ReportType: 'ddos' } // ✅ Also works -``` - ---- - -## Issue 4: Timestamp Validation ✅ - -**Problem**: Invalid timestamps like 'foo' not rejected - -**Solution**: Enhanced timestamp validation -- **File**: `src/validator.ts:327-387` -- Validates `timestamp` field as ISO 8601 -- Validates `occurrence.start` and `occurrence.end` -- Rejects invalid formats with clear error messages - -**Test Results**: ✅ 2/2 tests passing -```typescript -// Now properly validates -{ timestamp: 'foo' } -// ❌ Error: "Invalid timestamp format" - -{ occurrence: { start: 'foo', end: '2025-12-16T...' }} -// ❌ Error: "Invalid timestamp format for occurrence start" -``` - -**Validation Rules**: -- Must be parseable as JavaScript Date -- Both start and end must be valid ISO 8601 -- Start time must be before end time -- Invalid dates trigger validation errors - ---- - -## Issue 5: Generator Cannot Create Invalid Reports ✅ - -**Problem**: Generator could create reports that fail validation - -**Solution**: Added `validateCategoryRequirements()` method -- **File**: `src/generator.ts:389-447` -- Validates required fields BEFORE creating report -- Throws `XARFError` immediately if fields missing -- All generated reports guaranteed valid - -**Test Results**: ✅ 3/3 tests passing -```typescript -// Now enforces requirements at generation time - -// Content reports REQUIRE url -generator.generateReport({ - category: 'content', - type: 'phishing_site', - // Missing url -}); -// ❌ Throws: "url is required for content reports" - -// Connection reports REQUIRE destination_ip and protocol -generator.generateReport({ - category: 'connection', - type: 'ddos', - // Missing destination_ip, protocol -}); -// ❌ Throws: "destination_ip is required for connection reports" -``` - -**Category Requirements**: -- **connection**: `destination_ip`, `protocol` -- **content**: `url` -- **messaging**: `smtp_from` (when protocol=smtp), `subject` (for spam/phishing) -- **infrastructure**: No strict requirements -- **copyright**: No strict requirements -- **vulnerability**: No strict requirements -- **reputation**: No strict requirements - ---- - -## Verification - -### All Tests Passing ✅ -```bash -PASS tests/senior-engineer-feedback.test.ts - Senior Engineer Feedback Issues - Issue 1: Snake case vs camel case support - ✓ should accept snake_case properties from XARF spec examples - ✓ should work with XARF spec example directly copied - Issue 2: Invalid properties should emit warnings - ✓ should warn when using incorrect property names - ✓ should warn about misspelled category-specific fields - Issue 3: ReportType should alias to type - ✓ should accept ReportType as alias for type field - Issue 4: Timestamp validation should enforce ISO format - ✓ should throw error for invalid timestamp format - ✓ should reject invalid occurrence timestamps - Issue 5: Generator should not create invalid reports - ✓ should not allow creating reports missing required category fields - ✓ should validate generated reports pass XARFValidator - ✓ should enforce required fields at generation time for all categories - -Test Suites: 1 passed, 1 total -Tests: 10 passed, 10 total -``` - -### Full Test Suite ✅ -``` -Test Suites: 9 passed, 9 total -Tests: 152 passed, 152 total -``` - ---- - -## Files Modified - -1. **src/parser.ts** - - Added `checkForUnknownProperties()` method - - Emits warnings for unknown/misspelled fields - - All XARF spec fields validated - -2. **src/validator.ts** - - Enhanced occurrence timestamp validation - - Validates ISO 8601 format for start/end - - Clear error messages for invalid timestamps - -3. **src/generator.ts** - - Added `validateCategoryRequirements()` method - - Pre-validates required fields before generation - - Throws errors immediately for missing fields - - Updated `generateSampleReport()` to include required fields - -4. **tests/senior-engineer-feedback.test.ts** (NEW) - - Comprehensive test suite for all 5 issues - - 10 tests covering all scenarios - - All tests passing - -5. **tests/generator.test.ts** - - Updated to include required category fields - - All existing tests still passing - -6. **tests/generator.edge-cases.test.ts** - - Updated to include required category fields - - All existing tests still passing - ---- - -## Summary - -✅ **All 5 senior engineer feedback issues resolved** -✅ **10 new tests added - all passing** -✅ **152 total tests passing** -✅ **Zero breaking changes to existing functionality** -✅ **Production ready** - -The library now provides: -- Better developer experience with clear warnings -- Stronger validation at both parse and generation time -- XARF v4.0.0 spec compliance -- Prevents common mistakes (typos, missing fields) -- Clear, actionable error messages diff --git a/.github/WORKFLOWS_STATUS.md b/.github/WORKFLOWS_STATUS.md deleted file mode 100644 index 65be75d..0000000 --- a/.github/WORKFLOWS_STATUS.md +++ /dev/null @@ -1,55 +0,0 @@ -# GitHub Workflows Status - -## ✅ All Active Workflows Passing - -**Last Updated**: 2025-12-16 - -### Active Workflows (4) - -1. **CI** - ✅ PASSING - - Triggers: push, pull_request - - Jobs: Test (Node 16/18/20/22), Lint, Security Audit - - Status: All passing - -2. **Security Scanning** - ✅ PASSING - - Triggers: push, pull_request, schedule (daily), workflow_dispatch - - Jobs: - - ✅ Secret Scanning (TruffleHog CLI) - - ✅ OWASP Dependency Check - - ✅ NPM Audit - - ✅ License Compliance - - ✅ Security Summary - - Status: All passing - -3. **Dependency Review** - ✅ CONFIGURED - - Triggers: pull_request only - - Validates dependencies on PRs - - Status: Configured correctly - -4. **Publish to npm** - ✅ CONFIGURED - - Triggers: workflow_dispatch only (manual) - - Publishes package to npm registry - - Status: Ready for use - -### Removed Workflows (2) - -**CodeQL** and **OpenSSF Scorecard** were removed because they require GitHub Advanced Security, which is a paid feature for private repositories. - -### Test Results - -- **Unit Tests**: 152/152 passing (9 test suites) -- **TypeScript Build**: Successful -- **Linting**: 0 errors (39 acceptable warnings in test files) - -## Commit History - -- `aae89bb` - Remove incompatible workflows, fix secret scanning -- `170069f` - Disable workflows incompatible with private repos -- `e456fff` - Resolve all failing security workflow issues -- `ed1214d` - Resolve Dependabot workflow failures -- `d05cb7e` - Comprehensive security hardening and automation -- `87d3514` - Fix senior engineer feedback (all 5 issues) - -## Production Ready ✅ - -All workflows that can run on a private repository are now configured and passing. diff --git a/.github/security-recommendations.md b/.github/security-recommendations.md deleted file mode 100644 index 7bbb127..0000000 --- a/.github/security-recommendations.md +++ /dev/null @@ -1,310 +0,0 @@ -# Security Recommendations for XARF JavaScript Library - -This document provides comprehensive security recommendations for maintaining and improving the security posture of the XARF JavaScript library. - -## Table of Contents - -1. [Repository Security](#repository-security) -2. [Dependency Management](#dependency-management) -3. [Code Security](#code-security) -4. [CI/CD Security](#cicd-security) -5. [Vulnerability Response](#vulnerability-response) -6. [Security Monitoring](#security-monitoring) - -## Repository Security - -### Branch Protection Rules - -**Recommended settings for `main` branch:** - -```yaml -Require pull request reviews: ✓ - Required approving reviews: 2 - Dismiss stale reviews: ✓ - Require review from Code Owners: ✓ - -Require status checks before merging: ✓ - Required checks: - - CI / Test Node.js 20 - - CI / Lint and Format - - CI / Security Audit - - CodeQL / Analyze - - Dependency Review - -Require branches to be up to date: ✓ -Require signed commits: ✓ (recommended) -Include administrators: ✓ -Restrict pushes: ✓ - - Only allow specified users/teams - - xarf/maintainers -``` - -### Security Features to Enable - -Enable these GitHub repository settings: - -1. **Dependabot alerts** - ✓ Already enabled -2. **Dependabot security updates** - ✓ Enable for automatic fixes -3. **Secret scanning** - ✓ Enable (private repos) -4. **Push protection** - ✓ Prevent accidental secret commits -5. **Code scanning (CodeQL)** - ✓ Already configured -6. **Dependency graph** - ✓ Enable for better insights - -### Access Control - -- **Minimum permission principle**: Grant least privilege needed -- **Team-based access**: Use GitHub teams, not individual access -- **2FA required**: Enforce for all contributors -- **Deploy keys**: Use for automated deployments only -- **Personal access tokens**: Regular rotation, scoped permissions - -## Dependency Management - -### Current Configuration - -✅ **Dependabot** configured for: -- Daily npm dependency checks -- Weekly GitHub Actions updates -- Automatic grouping of minor/patch updates -- Security updates prioritized - -### Best Practices - -1. **Review before merging** - - Check changelogs for breaking changes - - Review security advisories - - Test thoroughly in staging - -2. **Pin dependencies** (when needed) - ```json - { - "resolutions": { - "vulnerable-package": "^1.2.3" - } - } - ``` - -3. **Audit regularly** - ```bash - npm audit - npm audit fix - npm audit fix --force # Use with caution - ``` - -4. **License compliance** - - Monitor license changes - - Block copyleft licenses (already configured) - - Keep license inventory updated - -## Code Security - -### Secure Coding Practices - -1. **Input Validation** - ```typescript - // ✅ Good: Validate all inputs - function parse(input: unknown): XARFReport { - if (typeof input !== 'object' || input === null) { - throw new ValidationError('Invalid input type'); - } - // Validate structure... - } - - // ❌ Bad: Assume input is valid - function parse(input: any): XARFReport { - return input as XARFReport; - } - ``` - -2. **Avoid Dynamic Code Execution** - - Never use `eval()` - - Be cautious with `Function()` constructor - - Sanitize user input before processing - -3. **Error Handling** - ```typescript - // ✅ Good: Don't leak sensitive information - catch (error) { - logger.error('Parse failed', { - reason: error.message // Generic message only - }); - throw new UserFacingError('Invalid XARF format'); - } - - // ❌ Bad: Expose internal details - catch (error) { - throw error; // May contain sensitive paths, data - } - ``` - -4. **Data Sanitization** - - Redact PII in logs - - Sanitize error messages - - Validate email formats - - Check URL schemes - -### TypeScript Security - -1. **Strict mode**: Already enabled in `tsconfig.json` - ```json - { - "compilerOptions": { - "strict": true, - "noImplicitAny": true, - "strictNullChecks": true - } - } - ``` - -2. **Type safety** - - Avoid `any` types (except in tests) - - Use branded types for sensitive data - - Validate at runtime, not just compile-time - -## CI/CD Security - -### Current Workflows - -✅ **Security workflows configured:** -- CodeQL analysis (2x weekly + on push) -- Dependency Review (on PRs) -- Security Scanning (daily + on push) -- OSSF Scorecard (weekly) - -### Workflow Permissions - -All workflows follow **principle of least privilege**: - -```yaml -permissions: - contents: read # Default - security-events: write # Only for security uploads - pull-requests: write # Only for PR comments -``` - -### Action Security - -1. **Pin actions to commit SHA** (most secure) - ```yaml - # ✅ Best: Pin to commit SHA - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - - # ⚠️ Acceptable: Pin to major version - uses: actions/checkout@v4 - - # ❌ Avoid: Floating tags - uses: actions/checkout@main - ``` - -2. **Review third-party actions** - - Check source code - - Verify maintainer reputation - - Monitor for updates - -3. **Secrets management** - - Use GitHub Secrets - - Rotate regularly - - Scope appropriately - - Never log secrets - -## Vulnerability Response - -### Process - -1. **Detection** - - Dependabot alerts - - CodeQL findings - - Security researcher reports - - User reports - -2. **Triage** (within 48 hours) - - Assess severity (CVSS score) - - Determine impact - - Plan response - -3. **Fix** (timeline by severity) - - Critical: 24-48 hours - - High: 1 week - - Moderate: 2 weeks - - Low: Next release - -4. **Disclosure** - - Create GitHub Security Advisory - - Coordinate with reporter - - Release patch - - Publish advisory - - Update SECURITY.md - -### Severity Levels - -| Level | CVSS | Response Time | Example | -|-------|------|---------------|---------| -| Critical | 9.0-10.0 | 24-48 hours | RCE, Auth bypass | -| High | 7.0-8.9 | 1 week | Data exposure, XSS | -| Moderate | 4.0-6.9 | 2 weeks | DoS, Info leak | -| Low | 0.1-3.9 | Next release | Minor info disclosure | - -## Security Monitoring - -### Automated Monitoring - -✅ **Currently implemented:** -- Daily security scans -- Twice-weekly CodeQL analysis -- Daily dependency checks -- Weekly OSSF Scorecard -- PR-based dependency review - -### Manual Reviews - -**Monthly:** -- Review open security alerts -- Check OSSF Scorecard results -- Review access logs -- Update dependencies - -**Quarterly:** -- Comprehensive security audit -- Review and update SECURITY.md -- Test incident response procedures -- Review access controls - -**Annually:** -- External security audit -- Penetration testing -- Security policy review -- Team security training - -### Metrics to Track - -1. **Vulnerability metrics** - - Mean time to detect (MTTD) - - Mean time to resolve (MTTR) - - Number of open vulnerabilities - - Severity distribution - -2. **Dependency metrics** - - Dependencies count - - Outdated dependencies - - Known vulnerabilities - - License compliance - -3. **Code metrics** - - CodeQL findings - - Test coverage - - Code complexity - - Security test coverage - -## Additional Resources - -- [OWASP Top 10](https://owasp.org/www-project-top-ten/) -- [Node.js Security Best Practices](https://nodejs.org/en/docs/guides/security/) -- [npm Security Best Practices](https://docs.npmjs.com/security-best-practices) -- [GitHub Security Features](https://docs.github.com/en/code-security) -- [OSSF Scorecard](https://github.com/ossf/scorecard) - -## Contact - -For security questions or concerns: -- Email: security@xarf.org -- GitHub Security Advisories: [Report privately](https://github.com/xarf/xarf-javascript/security/advisories/new) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43ff7cb..70ccf5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.0] - 2025-11-30 ### Breaking Changes + - **Category Correction**: Removed "other" category to align with XARF v4.0.0 specification - XARF spec defines exactly 7 categories (not 8) - Unknown v3 report types now map to `content` category with type `unclassified` - Migration: Replace any usage of `category: 'other'` with `category: 'content'` ### Added + - **Production Release**: Full production-quality v1.0.0 release - **Complete Category Support**: All 7 XARF categories fully implemented and validated - messaging, connection, content, infrastructure, copyright, vulnerability, reputation @@ -22,17 +24,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **CI/CD Improvements**: Enhanced GitHub Actions workflows with security scanning ### Changed + - **Version**: Updated from v1.0.0-alpha.2 to v1.0.0 (production release) - **Category Validation**: Stricter validation for all 7 official categories - **v3 Legacy Mapping**: Unknown v3 types now map to `content/unclassified` instead of `other/unclassified` - **Documentation**: Updated all references from 8 to 7 categories ### Fixed + - **Specification Compliance**: Corrected category count to match XARF v4.0.0 specification exactly ## [1.0.0-alpha.2] - 2025-01-23 ### Added + - **Backward Compatibility**: Full XARF v3 legacy format support - Automatic detection of v3 reports via `Version` field - Seamless conversion from v3 to v4 format @@ -52,17 +57,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Malware → content/malware - Botnet → infrastructure/botnet - Copyright → copyright/copyright -- **Legacy Metadata**: Converted reports include `_internal.legacy_version` field +- **Legacy Metadata**: Converted reports include top-level `legacy_version` field - **Comprehensive Tests**: 20+ new tests for v3 compatibility (142 total tests) - **Migration Guide**: Complete MIGRATION.md documentation - **TypeScript Types**: Full type definitions for v3 format structures ### Changed + - Parser now automatically converts v3 reports to v4 before validation - Validator accepts both v3 and v4 formats - Enhanced error messages for missing source identifiers in v3 reports ### Documentation + - Updated README with v3 compatibility examples - Added backward compatibility section with code samples - Created comprehensive MIGRATION.md guide @@ -71,6 +78,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.0-alpha.1] - 2024-01-15 ### Added + - Initial alpha release of XARF JavaScript/TypeScript library - XARFParser for parsing and validating XARF v4.0.0 reports - XARFGenerator for creating XARF-compliant reports @@ -93,6 +101,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Complete documentation and examples ### Notes + - Alpha release with focus on messaging, connection, and content categories - Additional categories have type definitions but limited validation - Full category support and validation planned for beta release diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..106dc57 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,77 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [https://abusix.com/contact](https://abusix.com/contact). All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). + +For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a32bbb8..178f5ad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,17 +50,20 @@ We actively welcome pull requests! Here's how to contribute: ### Getting Started 1. **Clone your fork:** + ```bash git clone https://github.com/YOUR_USERNAME/xarf-javascript.git cd xarf-javascript ``` 2. **Install dependencies:** + ```bash npm install ``` 3. **Build the project:** + ```bash npm run build ``` @@ -86,7 +89,7 @@ We actively welcome pull requests! Here's how to contribute: All contributions must maintain or improve test coverage: -- **Minimum coverage**: 80% for all code +- **Coverage thresholds** are enforced by Jest — see [jest.config.js](jest.config.js) for current values - **Unit tests**: Required for all new functions and classes - **Integration tests**: Required for parser and generator functionality - **Test file location**: Tests should be in the `tests/` directory @@ -95,17 +98,9 @@ All contributions must maintain or improve test coverage: ### Running Tests ```bash -# Run all tests -npm test - -# Run tests in watch mode during development -npm run test:watch - -# Generate coverage report -npm run test:coverage - -# View coverage report -open coverage/lcov-report/index.html +npm test # Run all tests +npm run test:watch # Run tests in watch mode during development +npm run test:coverage # Generate coverage report ``` ### Writing Tests @@ -113,26 +108,25 @@ open coverage/lcov-report/index.html We use Jest for testing. Example test structure: ```typescript -import { XarfParser } from '../src/parser'; - -describe('XarfParser', () => { - describe('parse', () => { - it('should parse a valid XARF report', () => { - const input = { - // ... valid XARF data - }; - - const result = XarfParser.parse(input); - - expect(result.version).toBe('4.0'); - expect(result.reportType).toBeDefined(); - }); - - it('should throw an error for invalid data', () => { - expect(() => { - XarfParser.parse({}); - }).toThrow(); - }); +import { parse } from '../src/parser'; + +describe('parse', () => { + it('should parse a valid XARF report', () => { + const input = { + // ... valid XARF data + }; + + const { report, errors } = parse(input); + + expect(errors).toHaveLength(0); + expect(report.category).toBeDefined(); + expect(report.type).toBeDefined(); + }); + + it('should return errors for invalid data', () => { + const { errors } = parse({}); + + expect(errors.length).toBeGreaterThan(0); }); }); ``` @@ -142,48 +136,38 @@ describe('XarfParser', () => { ### TypeScript Standards - **Language version**: TypeScript 5.3+ -- **Target**: ES2020 or higher -- **Module system**: ES Modules -- **Strict mode**: Enabled (`strict: true` in tsconfig.json) +- **Target**: ES2020 +- **Module system**: CommonJS +- **Strict mode**: Enabled + +See [tsconfig.json](tsconfig.json) for the full compiler configuration. ### Naming Conventions -- **Classes**: PascalCase (e.g., `XarfParser`, `XarfValidator`) -- **Functions/Methods**: camelCase (e.g., `parseReport`, `validateSchema`) -- **Constants**: UPPER_SNAKE_CASE (e.g., `DEFAULT_VERSION`, `MAX_RETRIES`) -- **Interfaces**: PascalCase with descriptive names (e.g., `XarfReport`, `ParserOptions`) -- **Type aliases**: PascalCase (e.g., `ReportType`, `Severity`) +- **Functions**: camelCase (e.g., `parse`, `createReport`, `createEvidence`) +- **Constants**: UPPER_SNAKE_CASE (e.g., `SPEC_VERSION`) +- **Interfaces/Types**: PascalCase with descriptive names (e.g., `ParseResult`, `XARFReport`, `ReportInput`) +- **Type aliases**: PascalCase (e.g., `ReportInput`, `ConnectionReportInput`) ### Code Organization -- **One class per file** for main components +- **One module per file** for main components - **Related types** can be grouped in a single file - **Export from index.ts** for public API - **Use barrel exports** for cleaner imports -### Formatting - -We use Prettier for code formatting with the following configuration: - -- **Single quotes** for strings -- **2 spaces** for indentation -- **No semicolons** (unless required) -- **Trailing commas** in multi-line structures -- **100 character** line length limit +### Formatting and Linting -Run `npm run format` before committing to ensure consistent formatting. +We use Prettier for formatting and ESLint with TypeScript support for linting. Configuration lives in [.prettierrc](.prettierrc) and [.eslintrc.json](.eslintrc.json). -### Linting - -We use ESLint with TypeScript support. Key rules: - -- **No unused variables** or imports -- **Explicit return types** for public functions -- **Prefer const** over let when variables aren't reassigned -- **No `any` types** without justification (use `unknown` or specific types) -- **Consistent error handling** with proper error types +```bash +npm run format:check # Check formatting +npm run format # Auto-format +npm run lint # Check linting +npm run lint:fix # Auto-fix linting issues +``` -Run `npm run lint` to check for issues and `npm run lint:fix` to auto-fix when possible. +A pre-commit hook (via Husky + lint-staged) runs both automatically on staged files. ### Documentation @@ -192,34 +176,6 @@ Run `npm run lint` to check for issues and `npm run lint:fix` to auto-fix when p - **Inline comments** for complex logic - **README updates** for new features -Example JSDoc: - -```typescript -/** - * Parse a XARF report from a JSON object or string - * - * @param input - The XARF report data as object or JSON string - * @param options - Optional parser configuration - * @returns Parsed and validated XARF report - * @throws {XarfParseError} If the input is invalid or malformed - * - * @example - * ```typescript - * const report = XarfParser.parse({ - * version: '4.0', - * reportType: 'abuse', - * // ... other fields - * }); - * ``` - */ -export function parse( - input: string | object, - options?: ParserOptions -): XarfReport { - // Implementation -} -``` - ## Commit Message Conventions We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification: diff --git a/FIXES_SUMMARY.md b/FIXES_SUMMARY.md deleted file mode 100644 index eadbd74..0000000 --- a/FIXES_SUMMARY.md +++ /dev/null @@ -1,150 +0,0 @@ -# XARF v4.0.0 Validation - Complete Fix Summary - -## Final Result: 100% Success Rate! 🎉 - -``` -BEFORE: 2/32 examples passing (6.3%) ❌ -AFTER: 34/34 examples passing (100.0%) ✅ -``` - -## Problems Identified - -### 1. Schema Examples Issues (30 files) -- **Missing `sender` field** - Required by XARF v4.0.0 spec -- **Incomplete `reporter` object** - Missing `domain` field, had invalid `type` field -- **Invalid `report_id` format** - Used prefixed strings instead of UUID v4 -- **Invalid `evidence_source` values** - Used custom values not in spec - -### 2. Generator Code Issues (2 bugs) -- **Hash format bug** - Generated `abc123` instead of `sha256:abc123` -- **Tags format bug** - Generated `['messaging', 'spam']` instead of `['category:messaging', 'type:spam']` - -### 3. Validator Code Issues (1 bug) -- **Restrictive evidence_source list** - Missing many allowed values from core schema - -## Fixes Applied - -### Phase 1: Schema Examples (AI Swarm - 30+ agents) -Deployed coordinated AI swarm to fix all 30 schema example files: - -1. **Added `sender` field to all examples** - ```json - "sender": { - "org": "Organization Name", - "contact": "abuse@example.com", - "domain": "example.com" - } - ``` - -2. **Fixed `reporter` object structure** - - Added required `domain` field - - Removed invalid `type` field - -3. **Fixed `report_id` to valid UUID format** - - Before: `"ddos-789a0123-b456-78c9-d012-345678901234"` - - After: `"789a0123-b456-48c9-a012-345678901234"` - -4. **Fixed `evidence_source` values** - - Updated to use only allowed values: spamtrap, user_complaint, automated_filter, honeypot, crawler, user_report, automated_scan, spam_analysis, firewall_logs, ids_detection, flow_analysis, vulnerability_scan, researcher_analysis, automated_discovery, traffic_analysis, threat_intelligence - -### Phase 2: Generator Fixes -Fixed code bugs in `/src/generator.ts`: - -1. **Hash format fix** - ```typescript - // Before: - const hash = this.generateHash(payloadBuffer, hashAlgorithm); - - // After: - const hashValue = this.generateHash(payloadBuffer, hashAlgorithm); - const hash = `${hashAlgorithm}:${hashValue}`; // Format: algorithm:hexvalue - ``` - -2. **Tags format fix** - ```typescript - // Before: - options.tags = [category, reportType, 'sample']; - - // After: - options.tags = [`category:${category}`, `type:${reportType}`, 'source:sample']; - ``` - -### Phase 3: Validator Update -Expanded evidence_source validation list in `/src/validator.ts`: -- Added 7 missing values to match xarf-core.json spec -- Total valid values: 18 (was 11) - -### Phase 4: Test Script Fix -Fixed `/validate-all-examples.js`: -- Generator method signature: `generateSampleReport(category, type, includeEvidence, includeOptional)` -- Was passing object as second parameter, now passes string type - -## Validation Breakdown - -### ✅ Schema Examples (25/25 - 100%) -All JSON schema examples from xarf-spec now validate: -- connection-* (6 files) -- content-* (9 files) -- copyright-* (6 files) -- infrastructure-* (2 files) -- messaging-* (2 files) -- reputation-* (2 files) -- vulnerability-* (3 files) - -### ✅ Generator Samples (7/7 - 100%) -All dynamically generated samples now validate: -- messaging/spam -- connection/ddos -- content/phishing -- infrastructure/botnet -- copyright/copyright -- vulnerability/cve -- reputation/blocklist - -### ✅ README Examples (2/2 - 100%) -Both hand-written README examples validate - -## Impact - -✅ **Users can now copy spec examples to create valid reports** -✅ **Validators implementing against these examples will be correct** -✅ **Spec compliance can be verified using spec's own examples** -✅ **Generator produces valid reports with correct formats** -✅ **All XARF v4.0.0 required fields are present and correct** - -## Files Modified - -### xarf-spec Repository (30 files) -- All files in `schemas/v4/types/*.json` - -### xarf-javascript Repository (4 files) -- `src/generator.ts` - Fixed hash and tags format -- `src/validator.ts` - Expanded evidence_source list -- `validate-all-examples.js` - Fixed generator method calls -- `SCHEMA_EXAMPLES_FIXED.md` - Documentation - -## Testing -```bash -# Run validation -node validate-all-examples.js - -# Expected output: -# Total examples tested: 34 -# Passed: 34 ✅ -# Failed: 0 ❌ -# Success rate: 100.0% -# ✅ ALL EXAMPLES VALIDATE! -``` - -## Date Completed -December 16, 2025 - -## Implementation Method -- AI Swarm Coordination (30+ specialized worker agents) -- Orchestrated via Claude Code + claude-flow MCP integration -- Human-like review process with context awareness -- File-by-file validation to prevent errors - ---- - -**Result: XARF v4.0.0 specification examples are now 100% compliant and valid! 🎉** diff --git a/MIGRATION.md b/MIGRATION.md deleted file mode 100644 index 61491dc..0000000 --- a/MIGRATION.md +++ /dev/null @@ -1,237 +0,0 @@ -# XARF v3 to v4 Migration Guide - -## Overview - -XARF v4 introduces a category-based architecture that improves upon the v3 format. This JavaScript library provides automatic backward compatibility, making migration seamless. - -## Automatic Conversion - -The library automatically detects and converts v3 reports to v4 format: - -```typescript -import { XARFParser } from 'xarf'; - -const parser = new XARFParser(); - -// v3 report is automatically converted -const report = parser.parse(v3JsonData); -``` - -## What Changes - -### Structure Changes - -**v3 Format:** - -```json -{ - "Version": "3", - "ReporterInfo": { - "ReporterOrg": "Security Team", - "ReporterOrgEmail": "abuse@example.com" - }, - "Report": { - "ReportType": "Spam", - "Date": "2024-01-15T10:00:00Z", - "SourceIp": "192.0.2.1" - } -} -``` - -**v4 Format (after conversion):** - -```json -{ - "xarf_version": "4.0.0", - "report_id": "auto-generated-uuid", - "timestamp": "2024-01-15T10:00:00Z", - "reporter": { - "org": "Security Team", - "contact": "abuse@example.com", - "type": "manual" - }, - "source_identifier": "192.0.2.1", - "category": "messaging", - "type": "spam", - "evidence_source": "manual_analysis", - "_internal": { - "legacy_version": "3", - "original_report_type": "Spam", - "converted_at": "2024-01-15T10:05:00Z" - } -} -``` - -### Field Mappings - -| v3 Field | v4 Field | Notes | -| --------------------------------------- | ------------------- | --------------------------- | -| `Version` | `xarf_version` | Set to "4.0.0" | -| N/A | `report_id` | Auto-generated UUID | -| `ReporterInfo.ReporterOrg` | `reporter.org` | Direct mapping | -| `ReporterInfo.ReporterOrgEmail` | `reporter.contact` | Direct mapping | -| N/A | `reporter.type` | Set to "manual" for v3 | -| `Report.Date` | `timestamp` | Direct mapping | -| `Report.SourceIp` or `Report.Source.IP` | `source_identifier` | Uses Source.IP if available | -| `Report.ReportType` | `category` + `type` | Mapped per table below | -| `Report.Attachment` or `Report.Samples` | `evidence` | Structure converted | -| N/A | `evidence_source` | Default: "manual_analysis" | - -### Report Type Mappings - -| v3 ReportType | v4 Category | v4 Type | -| -------------- | ---------------- | -------------- | -| `Spam` | `messaging` | `spam` | -| `Login-Attack` | `connection` | `login_attack` | -| `Port-Scan` | `connection` | `port_scan` | -| `DDoS` | `connection` | `ddos` | -| `Phishing` | `content` | `phishing` | -| `Malware` | `content` | `malware` | -| `Botnet` | `infrastructure` | `botnet` | -| `Copyright` | `copyright` | `copyright` | - -## Deprecation Warnings - -When parsing v3 reports, you'll receive deprecation warnings: - -```typescript -const parser = new XARFParser(); -const report = parser.parse(v3Report); - -const warnings = parser.getWarnings(); -// [ -// "DEPRECATION WARNING: XARF v3 format detected. The v3 format has been automatically converted to v4. Please update your systems to generate v4 reports directly. v3 support will be removed in a future major version.", -// ...conversion warnings... -// ] -``` - -## Migration Strategies - -### Phase 1: Accept Both Formats - -Use the library's automatic conversion: - -```typescript -const parser = new XARFParser(); - -function processReport(jsonData: unknown) { - const report = parser.parse(jsonData); - - if (report._internal?.legacy_version === '3') { - console.log('Received v3 report - consider upgrading sender'); - } - - // Process as v4 report - return handleV4Report(report); -} -``` - -### Phase 2: Monitor v3 Usage - -Track v3 report usage to plan deprecation: - -```typescript -function trackLegacyUsage(jsonData: unknown) { - const parser = new XARFParser(); - const report = parser.parse(jsonData); - - if (report._internal?.legacy_version === '3') { - metrics.increment('xarf.v3.reports'); - logDeprecationNotice(report.reporter.contact); - } -} -``` - -### Phase 3: Generate v4 Reports - -Update your report generators to produce v4 format: - -```typescript -import { XARFGenerator } from 'xarf'; - -const generator = new XARFGenerator(); - -const report = generator.generateReport({ - category: 'messaging', - type: 'spam', - source_identifier: '192.0.2.100', - reporter: { - org: 'Security Team', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Security Team', - contact: 'abuse@example.com', - domain: 'example.com', - }, - // ... additional fields -}); -``` - -## Testing Migration - -Test your v3 reports with the converter: - -```typescript -import { convertV3toV4, isXARFv3 } from 'xarf'; - -describe('v3 Migration', () => { - it('should convert our v3 reports', () => { - const v3Report = loadLegacyReport(); - - expect(isXARFv3(v3Report)).toBe(true); - - const warnings: string[] = []; - const v4Report = convertV3toV4(v3Report, warnings); - - expect(v4Report.xarf_version).toBe('4.0.0'); - expect(v4Report.category).toBeDefined(); - expect(v4Report.type).toBeDefined(); - - // Review any conversion warnings - warnings.forEach((warning) => console.log(warning)); - }); -}); -``` - -## Breaking Changes from v3 - -1. **Required Fields**: v4 requires `report_id` (UUID) - auto-generated during conversion -2. **Reporter Type**: v4 requires `reporter.type` - defaults to "manual" for v3 conversions -3. **Evidence Source**: v4 requires `evidence_source` - defaults to "manual_analysis" for v3 -4. **Category System**: v3's single `ReportType` becomes `category` + `type` in v4 -5. **Timestamp Format**: Both use ISO 8601, but v4 is more strict -6. **Evidence Structure**: v3's `Attachment`/`Samples` becomes structured `evidence` array - -## Unsupported v3 Features - -The following v3 fields have no direct v4 equivalent and are not preserved: - -- `Disclosure` - not included in v4 core spec -- `ReporterInfo.ReporterContactName` - not in v4 core spec -- `ReporterInfo.ReporterContactPhone` - not in v4 core spec - -If you need these fields, consider storing them in v4's `_internal` section: - -```typescript -const v4Report = convertV3toV4(v3Report); -v4Report._internal = { - ...v4Report._internal, - v3_disclosure: v3Report.Disclosure, - v3_contact_name: v3Report.ReporterInfo.ReporterContactName, -}; -``` - -## Getting Help - -- Check the [XARF v4 Specification](https://xarf.org) -- Review [API Documentation](https://github.com/xarf/xarf-javascript) -- Open an [Issue](https://github.com/xarf/xarf-javascript/issues) - -## Timeline - -- **Phase 1 (Current)**: Full v3 support with automatic conversion -- **Phase 2 (6 months)**: v3 support maintained, deprecation warnings -- **Phase 3 (12 months)**: Advanced notice of v3 support removal -- **Phase 4 (18 months)**: v3 support removed in next major version diff --git a/README.md b/README.md index 70cf836..31b29e9 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,20 @@ # XARF JavaScript/TypeScript Library -![XARF Spec](https://img.shields.io/badge/XARF%20Spec-v4.1.0-blue) +![XARF Spec](https://img.shields.io/badge/XARF%20Spec-v4.2.0-blue) [![npm version](https://badge.fury.io/js/xarf.svg)](https://www.npmjs.com/package/xarf) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Test](https://github.com/xarf/xarf-javascript/actions/workflows/test.yml/badge.svg)](https://github.com/xarf/xarf-javascript/actions/workflows/test.yml) -A comprehensive JavaScript/TypeScript library for parsing, validating, and generating XARF v4.0.0 (eXtended Abuse Reporting Format) reports. +A JavaScript/TypeScript library for parsing, validating, and generating [XARF v4](https://xarf.org) (eXtended Abuse Reporting Format) reports. ## Features -- **Parser**: Parse and validate XARF reports from JSON -- **Generator**: Create XARF-compliant reports programmatically -- **Validator**: Comprehensive validation with detailed error reporting -- **TypeScript Support**: Full type definitions for all XARF structures -- **Backward Compatibility**: Automatic v3 to v4 conversion with deprecation warnings -- **All Categories**: Support for all 7 XARF categories - - Messaging - - Connection - - Content - - Infrastructure - - Copyright - - Vulnerability - - Reputation +- **Parse** XARF reports from JSON with validation and typed results +- **Generate** XARF-compliant reports with auto-generated metadata (UUIDs, timestamps) +- **Validate** reports against the official JSON schemas with detailed errors and warnings +- **Full TypeScript support** with discriminated union types for all 7 categories +- **v3 backward compatibility** with automatic detection and conversion +- **Schema-driven** — validation rules derived from the official [xarf-spec](https://github.com/xarf/xarf-spec) schemas, not hardcoded ## Installation @@ -34,11 +27,10 @@ npm install xarf ### Parsing a Report ```typescript -import { XARFParser } from 'xarf'; +import { parse } from 'xarf'; -const parser = new XARFParser(); -const report = parser.parse({ - xarf_version: '4.0.0', +const { report, errors, warnings } = parse({ + xarf_version: '4.2.0', report_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', timestamp: '2024-01-15T10:30:00Z', reporter: { @@ -59,19 +51,29 @@ const report = parser.parse({ protocol: 'tcp', }); -console.log(report.category); // 'connection' +if (errors.length === 0) { + console.log(report.category); // 'connection' +} else { + console.log('Validation errors:', errors); +} ``` -### Generating a Report +### Creating a Report ```typescript -import { XARFGenerator } from 'xarf'; +import { createReport, createEvidence } from 'xarf'; + +// Returns { content_type, payload (base64), hash, size, description } +const evidence = createEvidence('message/rfc822', rawEmailContent, { + description: 'Original spam email', + hashAlgorithm: 'sha256', +}); -const generator = new XARFGenerator(); -const report = generator.generateReport({ +// xarf_version, report_id, and timestamp are auto-generated +const { report, errors, warnings } = createReport({ category: 'messaging', - type: 'spam', // Using XARF spec field name - source_identifier: '192.0.2.100', // snake_case matches XARF spec + type: 'spam', + source_identifier: '192.0.2.100', reporter: { org: 'Example Security', contact: 'abuse@example.com', @@ -82,444 +84,190 @@ const report = generator.generateReport({ contact: 'abuse@example.com', domain: 'example.com', }, - evidence_source: 'automated_scan', // snake_case matches XARF spec + evidence_source: 'spamtrap', description: 'Spam email detected from source', - tags: ['spam', 'email'], - // Category-specific fields can be passed directly (union types) protocol: 'smtp', smtp_from: 'spammer@evil.example.com', + evidence: [evidence], }); console.log(JSON.stringify(report, null, 2)); ``` -### Validating a Report +## API Reference -```typescript -import { XARFValidator } from 'xarf'; - -const validator = new XARFValidator(); -const result = validator.validate(report); - -if (result.valid) { - console.log('Report is valid'); -} else { - console.log('Validation errors:', result.errors); - console.log('Warnings:', result.warnings); -} -``` +### `parse(jsonData, options?)` -## API Documentation - -### XARFParser - -Parse and validate XARF reports from JSON. +Parse and validate a XARF report from JSON. Supports both v4 and v3 (legacy) formats — v3 reports are automatically converted to v4 with deprecation warnings. ```typescript -const parser = new XARFParser(strict?: boolean); -``` - -- `strict`: If `true`, throw exceptions on validation errors. If `false`, collect errors for retrieval. - -#### Methods - -- `parse(jsonData: string | object): XARFReport` - Parse a XARF report -- `validate(jsonData: string | object): boolean` - Validate without parsing -- `getErrors(): string[]` - Get validation errors from last operation +import { parse } from 'xarf'; -### XARFGenerator - -Generate XARF-compliant reports programmatically. - -```typescript -const generator = new XARFGenerator(); +const { report, errors, warnings, info } = parse(jsonData, options?); ``` -#### Methods +**Parameters:** -- `generateReport(options: GeneratorOptions): XARFReport` - Create a complete report -- `generateUUID(): string` - Generate a UUID for report ID -- `generateTimestamp(): string` - Generate an ISO 8601 timestamp -- `generateHash(data: string | Buffer, algorithm?: string): string` - Hash data -- `addEvidence(contentType: string, description: string, payload: string | Buffer): XARFEvidence` - Create evidence with hash -- `generateRandomEvidence(category: XARFCategory, description?: string): XARFEvidence` - Generate sample evidence -- `generateSampleReport(category: XARFCategory, reportType: string, includeEvidence?: boolean, includeOptional?: boolean): XARFReport` - Generate test report +- `jsonData: string | Record` — JSON string or object containing a XARF report +- `options.strict?: boolean` — Throw `XARFValidationError` on validation failures (default: `false`) +- `options.showMissingOptional?: boolean` — Include info about missing optional fields (default: `false`) -#### GeneratorOptions +**Returns `ParseResult`:** -The `GeneratorOptions` interface uses **snake_case** field names, matching the XARF specification. +- `report: XARFReport` — The parsed report, typed by category +- `errors: string[]` — Validation errors (empty if valid) +- `warnings: string[]` — Validation warnings +- `info?: ValidationInfo[]` — Missing optional field info (only when `showMissingOptional` is `true`) -**Union Types**: Category-specific fields can be passed directly without using `additionalFields`: +### `createReport(input, options?)` -```typescript -// Messaging report with direct fields -const messagingReport = generator.generateReport({ - category: 'messaging', - type: 'spam', - source_identifier: '192.0.2.100', - reporter: { org: '...', contact: '...', domain: '...' }, - sender: { org: '...', contact: '...', domain: '...' }, - evidence_source: 'spamtrap', - // Messaging-specific fields directly on options - protocol: 'smtp', - smtp_from: 'spammer@evil.example.com', - subject: 'You won!', -}); - -// Connection report with direct fields -const connectionReport = generator.generateReport({ - category: 'connection', - type: 'ddos', - source_identifier: '192.0.2.100', - reporter: { org: '...', contact: '...', domain: '...' }, - sender: { org: '...', contact: '...', domain: '...' }, - evidence_source: 'honeypot', - // Connection-specific fields directly on options - destination_ip: '203.0.113.10', - protocol: 'tcp', - destination_port: 80, -}); -``` - -**Base Options** (all categories): +Create a validated XARF report with auto-generated metadata. Automatically fills `xarf_version`, `report_id` (UUID), and `timestamp` (ISO 8601) if not provided. ```typescript -{ - category: XARFCategory; // Required: Report category - type?: string; // Report type (e.g., 'spam', 'ddos') - source_identifier?: string; // Required: Source IP or identifier - reporter: ContactInfo; // Required: Reporter information - sender: ContactInfo; // Required: Sender information - evidence_source?: EvidenceSource; // Evidence source - description?: string; // Human-readable description - evidence?: XARFEvidence[]; // Evidence items - confidence?: number; // Confidence score (0.0 to 1.0) - tags?: string[]; // Tags for categorization - additionalFields?: Record; // Additional fields -} -``` - -### XARFValidator +import { createReport } from 'xarf'; -Comprehensive validation with detailed error and warning reporting. - -```typescript -const validator = new XARFValidator(); +const { report, errors, warnings } = createReport(input, options?); ``` -#### Methods - -- `validate(report: XARFReport, strict?: boolean, showMissingOptional?: boolean): ValidationResult` - Validate a report +**Parameters:** -Parameters: +- `input: ReportInput` — Report data. A discriminated union on `category` that narrows type-safe fields per category (e.g., `MessagingReportInput`, `ConnectionReportInput`, etc.) +- `options.strict?: boolean` — Throw on validation failures (default: `false`) +- `options.showMissingOptional?: boolean` — Include info about missing optional fields (default: `false`) -- `report` - The XARF report to validate -- `strict` - If `true`, throw `XARFValidationError` on validation failures (default: `false`) -- `showMissingOptional` - If `true`, include info about missing optional fields (default: `false`) +**Returns `CreateReportResult`:** -Returns: +- `report: XARFReport` — The generated report +- `errors: ValidationError[]` — Structured validation errors (`{ field, message, value? }`) +- `warnings: ValidationWarning[]` — Structured validation warnings (`{ field, message, value? }`) +- `info?: ValidationInfo[]` — Missing optional field info (only when `showMissingOptional` is `true`) -```typescript -{ - valid: boolean; - errors: Array<{ field: string; message: string; value?: unknown }>; - warnings: Array<{ field: string; message: string; value?: unknown }>; - info?: Array<{ field: string; message: string }>; // Only when showMissingOptional=true -} -``` +### `createEvidence(contentType, payload, options?)` -#### Unknown Field Detection - -The validator automatically warns about unknown fields in reports: +Create an evidence object with automatic base64 encoding, hashing, and size calculation. ```typescript -const report = { - // ... valid fields ... - unknownField: 'value', // Will trigger a warning -}; +import { createEvidence } from 'xarf'; -const result = validator.validate(report); -// result.warnings will include: "Unknown field 'unknownField' is not defined in the XARF schema" +const evidence = createEvidence(contentType, payload, options?); ``` -In strict mode, unknown fields are treated as errors. - -#### Missing Optional Fields - -Use `showMissingOptional` to discover which optional fields you could add: +**Parameters:** -```typescript -const result = validator.validate(report, false, true); +- `contentType: string` — MIME type of the evidence (e.g., `'message/rfc822'`) +- `payload: string | Buffer` — The evidence data +- `options.description?: string` — Human-readable description +- `options.hashAlgorithm?: 'sha256' | 'sha512' | 'sha1' | 'md5'` — Hash algorithm (default: `'sha256'`) -if (result.info) { - result.info.forEach(({ field, message }) => { - console.log(`${field}: ${message}`); - // e.g., "description: OPTIONAL - Human-readable description of the abuse" - // e.g., "confidence: RECOMMENDED - Confidence score between 0.0 and 1.0" - }); -} -``` +**Returns `XARFEvidence`** with computed `hash`, `size`, and base64-encoded `payload`. -### SchemaRegistry +### `schemaRegistry` -Access schema-derived validation rules programmatically: +Access schema-derived validation rules and metadata programmatically. ```typescript import { schemaRegistry } from 'xarf'; // Get all valid categories -const categories = schemaRegistry.getCategories(); -// Set { 'messaging', 'connection', 'content', ... } +schemaRegistry.getCategories(); +// Set { 'messaging', 'connection', 'content', 'infrastructure', 'copyright', 'vulnerability', 'reputation' } // Get valid types for a category -const types = schemaRegistry.getTypesForCategory('connection'); +schemaRegistry.getTypesForCategory('connection'); // Set { 'ddos', 'port_scan', 'login_attack', ... } // Check if a category/type combination is valid schemaRegistry.isValidType('connection', 'ddos'); // true // Get valid evidence sources -const sources = schemaRegistry.getEvidenceSources(); +schemaRegistry.getEvidenceSources(); // Set { 'honeypot', 'spamtrap', 'user_report', ... } // Get field metadata including descriptions -const metadata = schemaRegistry.getFieldMetadata('confidence'); +schemaRegistry.getFieldMetadata('confidence'); // { description: '...', required: false, recommended: true, ... } ``` -## Categories and Types - -### Messaging - -- `spam`, `phishing`, `social_engineering`, `bulk_messaging` - -### Connection - -- `ddos`, `port_scan`, `login_attack`, `ip_spoofing`, `compromised`, `botnet`, `malicious_traffic`, and more - -### Content - -- `phishing_site`, `malware_distribution`, `defacement`, `spamvertised`, `web_hack`, and more - -### Infrastructure - -- `botnet`, `compromised_server` - -### Copyright - -- `infringement`, `dmca`, `trademark`, `p2p`, and more - -### Vulnerability - -- `cve`, `misconfiguration`, `open_service` - -### Reputation - -- `blocklist`, `threat_intelligence` +### Validation Details -## Examples +Both `parse()` and `createReport()` run validation internally. Additional behaviors: -### Connection Report (DDoS) +- **Unknown fields** trigger warnings (or errors in strict mode) +- **Missing optional fields** can be discovered with `showMissingOptional: true`: ```typescript -const report = generator.generateReport({ - category: 'connection', - type: 'ddos', - source_identifier: '192.0.2.100', - reporter: { - org: 'Security Operations', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Security Operations', - contact: 'abuse@example.com', - domain: 'example.com', - }, - evidence_source: 'honeypot', - // Category-specific fields directly (union types) - destination_ip: '203.0.113.10', - protocol: 'tcp', - destination_port: 80, - attack_type: 'syn_flood', - confidence: 0.95, -}); -``` - -### Content Report (Phishing Site) - -```typescript -const report = generator.generateReport({ - category: 'content', - type: 'phishing', - source_identifier: '192.0.2.100', - reporter: { - org: 'Phishing Response Team', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Phishing Response Team', - contact: 'abuse@example.com', - domain: 'example.com', - }, - evidence_source: 'user_report', - // Category-specific fields directly (union types) - url: 'http://phishing.example.com', - content_type: 'text/html', - description: 'Phishing site mimicking banking portal', - tags: ['phishing', 'banking', 'credential-theft'], -}); -``` - -### Messaging Report (Spam) +const { info } = parse(report, { showMissingOptional: true }); -```typescript -const report = generator.generateReport({ - category: 'messaging', - type: 'spam', - source_identifier: '192.0.2.100', - reporter: { - org: 'Example Security', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Example Security', - contact: 'abuse@example.com', - domain: 'example.com', - }, - evidence_source: 'spamtrap', - // Category-specific fields directly (union types) - protocol: 'smtp', - smtp_from: 'spammer@evil.example.com', - smtp_to: 'victim@example.com', - subject: 'You won the lottery!', - message_id: '<123456@evil.example.com>', -}); +if (info) { + info.forEach(({ field, message }) => { + console.log(`${field}: ${message}`); + // e.g., "description: OPTIONAL - Human-readable description of the abuse" + // e.g., "confidence: RECOMMENDED - Confidence score between 0.0 and 1.0" + }); +} ``` -## TypeScript Support +## v3 Backward Compatibility -Full TypeScript definitions are included: +The library automatically detects XARF v3 reports (by the `Version` field) and converts them to v4 during parsing. Converted reports include `legacy_version: '3'` and deprecation warnings. ```typescript -import type { - XARFReport, - ConnectionReport, - MessagingReport, - XARFCategory, - ReporterType, -} from 'xarf'; - -const report: ConnectionReport = { - // TypeScript will enforce correct structure -}; -``` +import { parse } from 'xarf'; -## Testing +const { report, warnings } = parse(v3Report); -```bash -# Run tests -npm test - -# Run tests with coverage -npm run test:coverage - -# Run tests in watch mode -npm run test:watch +console.log(report.xarf_version); // '4.2.0' +console.log(report.category); // mapped category (e.g., 'messaging') +console.log(report.legacy_version); // '3' +// warnings includes deprecation notice + conversion details ``` -## Building +You can also use the low-level utilities directly: -```bash -# Build TypeScript to JavaScript -npm run build +```typescript +import { isXARFv3, convertV3toV4, getV3DeprecationWarning } from 'xarf'; -# Type check without building -npm run typecheck +if (isXARFv3(jsonData)) { + const warnings: string[] = []; + const v4Report = convertV3toV4(v3Report, warnings); + console.log(getV3DeprecationWarning()); +} ``` +Unknown v3 report types cause a parse error listing the supported types. See [MIGRATION_V3_TO_V4.md](docs/MIGRATION_V3_TO_V4.md) for the full type mapping and migration strategies. + ## Schema Management -This library validates against the official [xarf-spec](https://github.com/xarf/xarf-spec) JSON schemas. Schemas are automatically fetched from the xarf-spec repository on `npm install`. +This library validates against the official [xarf-spec](https://github.com/xarf/xarf-spec) JSON schemas. Schemas are fetched automatically on `npm install` based on the version configured in `package.json`: -### Checking for Schema Updates +```json +"xarfSpec": { + "version": "v4.2.0" +} +``` ```bash # Check if a newer version of xarf-spec is available npm run check-schema-updates -# Show all available releases -npm run check-schema-updates -- --all -``` - -Example output: - -``` -[xarf] Checking for schema updates... - - Configured version: v4.1.0 - Installed version: v4.1.0 - Latest release: v4.1.0 (Dec 18, 2025) - - ✅ You are using the latest version. -``` - -### Updating Schemas - -To update to a newer version of the XARF specification: - -1. Edit `package.json` and update the version: - - ```json - "xarfSpec": { - "version": "v4.2.0" - } - ``` - -2. Run npm install to fetch the new schemas: - ```bash - npm install - ``` - -### Manual Schema Fetch - -```bash -# Re-fetch schemas from xarf-spec (useful if schemas are missing) +# Re-fetch schemas (e.g., if missing or to force a refresh) npm run fetch-schemas ``` -## Linting and Formatting - -```bash -# Run ESLint -npm run lint - -# Fix linting issues -npm run lint:fix +To update to a newer spec version, change the version in `package.json` and run `npm install`. -# Check Prettier formatting -npm run format:check +## Development -# Format code -npm run format +```bash +npm test # Run tests +npm run test:coverage # Run tests with coverage +npm run build # Build TypeScript to JavaScript +npm run typecheck # Type-check without emitting +npm run lint # Run ESLint +npm run format:check # Check Prettier formatting ``` -## Contributing - -Contributions are welcome! Please: - -1. Fork the repository -2. Create a feature branch -3. Add tests for new functionality -4. Ensure all tests pass -5. Run linting and formatting -6. Submit a pull request - -## License - -MIT License - see LICENSE file for details +See [CONTRIBUTING.md](CONTRIBUTING.md) for development guidelines. ## Links @@ -527,94 +275,5 @@ MIT License - see LICENSE file for details - [GitHub Repository](https://github.com/xarf/xarf-javascript) - [npm Package](https://www.npmjs.com/package/xarf) - [Issue Tracker](https://github.com/xarf/xarf-javascript/issues) - -## Backward Compatibility (v3 Legacy Support) - -This library automatically detects and converts XARF v3 format reports to v4: - -```typescript -import { XARFParser } from 'xarf'; - -const parser = new XARFParser(); - -// v3 format report -const v3Report = { - Version: '3', - ReporterInfo: { - ReporterOrg: 'Security Team', - ReporterOrgEmail: 'abuse@example.com', - }, - Report: { - ReportType: 'Spam', - Date: '2024-01-15T10:00:00Z', - SourceIp: '192.0.2.100', - Protocol: 'smtp', - SmtpMailFromAddress: 'spammer@evil.example', - }, -}; - -// Automatically converted to v4 format -const report = parser.parse(v3Report); -console.log(report.xarf_version); // '4.0.0' -console.log(report.category); // 'messaging' -console.log(report.type); // 'spam' -console.log(report._internal?.legacy_version); // '3' - -// Get deprecation warnings -const warnings = parser.getWarnings(); -console.log(warnings); // Contains deprecation notice -``` - -### v3 Format Detection - -The parser automatically detects v3 reports by checking for the `Version` field: - -```typescript -import { isXARFv3 } from 'xarf'; - -if (isXARFv3(jsonData)) { - console.log('This is a legacy v3 report'); -} -``` - -### Manual v3 Conversion - -You can also manually convert v3 reports: - -```typescript -import { convertV3toV4, getV3DeprecationWarning } from 'xarf'; - -const warnings: string[] = []; -const v4Report = convertV3toV4(v3Report, warnings); - -console.log(getV3DeprecationWarning()); -// "DEPRECATION WARNING: XARF v3 format detected..." -``` - -### v3 Type Mapping - -The following v3 report types are automatically mapped to v4 categories: - -| v3 ReportType | v4 Category | v4 Type | -| ------------- | -------------- | ------------ | -| Spam | messaging | spam | -| Login-Attack | connection | login_attack | -| Port-Scan | connection | port_scan | -| DDoS | connection | ddos | -| Phishing | content | phishing | -| Malware | content | malware | -| Botnet | infrastructure | botnet | -| Copyright | copyright | copyright | - -Unknown v3 report types are mapped to category `content` with type `unclassified`. - -## Version - -Current version: 1.0.0 -XARF Specification: 4.1.0 (from [xarf-spec](https://github.com/xarf/xarf-spec)) - -Production release with full support for all 7 XARF categories: messaging, connection, content, infrastructure, copyright, vulnerability, and reputation. - -**Schema Updates**: Schemas are fetched from the official xarf-spec repository. Run `npm run check-schema-updates` to check for new versions. - -**v3 Compatibility**: Full backward compatibility with XARF v3 format with automatic conversion and deprecation warnings. +- [Migration Guide (v3 → v4)](docs/MIGRATION_V3_TO_V4.md) +- [License (MIT)](LICENSE) diff --git a/RELEASE_NOTES_1.0.0.md b/RELEASE_NOTES_1.0.0.md deleted file mode 100644 index a2f6413..0000000 --- a/RELEASE_NOTES_1.0.0.md +++ /dev/null @@ -1,384 +0,0 @@ -# XARF JavaScript/TypeScript Library v1.0.0 Release Notes - -**Release Date**: November 30, 2025 - -## Overview - -We are excited to announce the production release of the XARF JavaScript/TypeScript library v1.0.0! This release marks the transition from alpha to production-ready status with full support for the XARF v4.0.0 specification. - -## What's New in v1.0.0 - -### Production Ready -- **Stable API**: Production-quality implementation with comprehensive testing -- **Full Category Support**: All 7 XARF categories fully implemented and validated -- **Enhanced Security**: Improved input validation and XSS prevention -- **Complete Documentation**: Comprehensive guides for all use cases - -### Specification Compliance -- **Exact Category Match**: Corrected to support exactly 7 XARF categories as per specification - - messaging - - connection - - content - - infrastructure - - copyright - - vulnerability - - reputation -- **No More "Other"**: Removed the non-standard "other" category for specification compliance - -### Key Features - -#### 1. XARF v4.0.0 Parser -```typescript -import { XARFParser } from 'xarf'; - -const parser = new XARFParser(); -const report = parser.parse(jsonData); -``` - -#### 2. Report Generator -```typescript -import { XARFGenerator } from 'xarf'; - -const generator = new XARFGenerator(); -const report = generator.generateReport({ - category: 'messaging', - reportType: 'spam', - sourceIdentifier: '192.0.2.100', - reporterContact: 'abuse@example.com', - reporterOrg: 'Security Operations' -}); -``` - -#### 3. Comprehensive Validator -```typescript -import { XARFValidator } from 'xarf'; - -const validator = new XARFValidator(); -const result = validator.validate(report); - -if (!result.valid) { - console.error('Validation errors:', result.errors); - console.warn('Warnings:', result.warnings); -} -``` - -#### 4. Backward Compatibility (v3 Legacy Support) -```typescript -// Automatic v3 to v4 conversion -const v3Report = { - Version: '3', - ReporterInfo: { /* ... */ }, - Report: { /* ... */ } -}; - -const parser = new XARFParser(); -const v4Report = parser.parse(v3Report); // Auto-converted! -``` - -### Breaking Changes from Alpha - -⚠️ **Important**: If you are upgrading from v1.0.0-alpha.x, please review these changes: - -1. **Removed "other" Category** - - The "other" category has been removed to align with the XARF v4.0.0 specification - - **Migration**: Replace any `category: 'other'` with `category: 'content'` and `type: 'unclassified'` - - Unknown v3 report types now map to `content/unclassified` instead of `other/unclassified` - -2. **Stricter Category Validation** - - Only the 7 official categories are now accepted - - Invalid categories will cause validation errors - -### TypeScript Support - -Full TypeScript type definitions included: - -```typescript -import type { - XARFReport, - XARFCategory, - MessagingReport, - ConnectionReport, - ContentReport, - InfrastructureReport, - CopyrightReport, - VulnerabilityReport, - ReputationReport, - AnyXARFReport -} from 'xarf'; -``` - -### All 7 XARF Categories - -#### 1. Messaging -Report types: `spam`, `phishing`, `social_engineering`, `bulk_messaging` - -```typescript -const report = generator.generateReport({ - category: 'messaging', - reportType: 'spam', - additionalFields: { - protocol: 'smtp', - smtp_from: 'spammer@evil.example', - subject: 'Spam email' - } -}); -``` - -#### 2. Connection -Report types: `ddos`, `port_scan`, `login_attack`, `ip_spoofing`, `compromised`, `botnet`, etc. - -```typescript -const report = generator.generateReport({ - category: 'connection', - reportType: 'ddos', - additionalFields: { - destination_ip: '203.0.113.10', - protocol: 'tcp', - attack_type: 'syn_flood' - } -}); -``` - -#### 3. Content -Report types: `phishing_site`, `malware_distribution`, `defacement`, `web_hack`, etc. - -```typescript -const report = generator.generateReport({ - category: 'content', - reportType: 'phishing_site', - additionalFields: { - url: 'http://phishing.example.com', - content_type: 'text/html' - } -}); -``` - -#### 4. Infrastructure -Report types: `botnet`, `compromised_server` - -#### 5. Copyright -Report types: `infringement`, `dmca`, `trademark`, `p2p` - -#### 6. Vulnerability -Report types: `cve`, `misconfiguration`, `open_service` - -#### 7. Reputation -Report types: `blocklist`, `threat_intelligence` - -## Installation - -```bash -npm install xarf -``` - -## Upgrade Guide - -### From v1.0.0-alpha.2 - -1. **Update package.json**: - ```bash - npm install xarf@1.0.0 - ``` - -2. **Check for "other" category usage**: - ```bash - # Search your codebase - grep -r "category.*other" . - ``` - -3. **Replace "other" with "content"**: - ```typescript - // Before (alpha) - { category: 'other', type: 'unclassified' } - - // After (v1.0.0) - { category: 'content', type: 'unclassified' } - ``` - -4. **Run tests**: - ```bash - npm test - ``` - -### From v3 Format - -The library provides automatic v3 to v4 conversion: - -```typescript -const parser = new XARFParser(); -const v4Report = parser.parse(v3Report); - -// Check for conversion warnings -const warnings = parser.getWarnings(); -warnings.forEach(warning => console.warn(warning)); -``` - -See [docs/MIGRATION_V3_TO_V4.md](docs/MIGRATION_V3_TO_V4.md) for detailed migration guide. - -## Testing - -Comprehensive test suite with 80%+ code coverage: - -```bash -# Run all tests -npm test - -# Run with coverage report -npm run test:coverage - -# Watch mode for development -npm run test:watch -``` - -## Code Quality - -```bash -# Linting -npm run lint - -# Type checking -npm run typecheck - -# Format checking -npm run format:check - -# Auto-format code -npm run format -``` - -## Security - -Security is a top priority: - -- **Input Validation**: All inputs are validated against the XARF v4.0.0 schema -- **XSS Prevention**: Proper escaping and sanitization -- **Dependency Scanning**: Regular security audits -- **No Known Vulnerabilities**: Clean security scan - -Run security audit: -```bash -npm audit -``` - -See [SECURITY.md](SECURITY.md) for security policy and reporting guidelines. - -## Documentation - -- **README.md**: Quick start and API overview -- **docs/MIGRATION_V3_TO_V4.md**: Comprehensive v3 to v4 migration guide -- **CHANGELOG.md**: Complete version history -- **SECURITY.md**: Security policy and best practices -- **API Documentation**: Inline TypeScript type definitions - -## Examples - -### Basic Spam Report -```typescript -import { XARFGenerator } from 'xarf'; - -const generator = new XARFGenerator(); -const report = generator.generateReport({ - category: 'messaging', - reportType: 'spam', - sourceIdentifier: '192.0.2.100', - reporterContact: 'abuse@example.com', - reporterOrg: 'Security Team', - evidenceSource: 'spamtrap', - severity: 'low', - tags: ['spam', 'email'] -}); - -console.log(JSON.stringify(report, null, 2)); -``` - -### DDoS Attack Report -```typescript -const report = generator.generateReport({ - category: 'connection', - reportType: 'ddos', - sourceIdentifier: '192.0.2.100', - reporterContact: 'security@example.com', - reporterOrg: 'Security Operations', - additionalFields: { - destination_ip: '203.0.113.10', - protocol: 'tcp', - destination_port: 80, - attack_type: 'syn_flood', - packet_count: 1000000 - }, - severity: 'critical', - confidence: 0.95 -}); -``` - -### Phishing Site Report -```typescript -const report = generator.generateReport({ - category: 'content', - reportType: 'phishing_site', - sourceIdentifier: '192.0.2.100', - reporterContact: 'phishing@example.com', - reporterOrg: 'Phishing Response Team', - additionalFields: { - url: 'http://phishing.example.com', - content_type: 'text/html' - }, - description: 'Phishing site mimicking banking portal', - tags: ['phishing', 'banking', 'credential-theft'], - severity: 'high' -}); -``` - -## Performance - -- **Fast Parsing**: Optimized JSON parsing and validation -- **Low Memory**: Efficient memory usage for large reports -- **TypeScript**: Full type safety with zero runtime overhead - -## Browser Support - -Works in all modern browsers and Node.js environments: - -- **Node.js**: 16.x, 18.x, 20.x, 22.x -- **Browsers**: Chrome, Firefox, Safari, Edge (ES2015+) - -## Contributing - -Contributions welcome! Please see our contributing guidelines: - -1. Fork the repository -2. Create a feature branch -3. Add tests for new functionality -4. Ensure all tests pass -5. Submit a pull request - -## License - -MIT License - see [LICENSE](LICENSE) file for details. - -## Links - -- **GitHub**: https://github.com/xarf/xarf-javascript -- **npm**: https://www.npmjs.com/package/xarf -- **XARF Specification**: https://xarf.org -- **Issue Tracker**: https://github.com/xarf/xarf-javascript/issues - -## Acknowledgments - -Thank you to all contributors and the XARF community for making this release possible! - -## What's Next? - -Future roadmap includes: - -- Additional validation rules -- Performance optimizations -- Enhanced documentation -- More example code -- Integration guides - -Stay tuned for updates! - ---- - -**Happy reporting! 🎉** diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 5631def..0000000 --- a/TODO.md +++ /dev/null @@ -1,70 +0,0 @@ -# xarf-javascript TODO - -## High Priority - -### JSON Schema Validation - Production Ready - -**Status:** Core schema validation ✅ PRODUCTION READY | Type-specific validation ⚠️ KNOWN LIMITATION - -**What Works:** - -- ✅ Core schema validation (`SchemaValidator.validateCore()`) -- ✅ All 207 tests passing (46 schema-specific tests) -- ✅ Format validation (email, UUID, ISO dates, hostnames) -- ✅ Range validation (confidence 0-1, ports 1-65535) -- ✅ maxLength, maxItems, additionalProperties enforcement -- ✅ Comprehensive error messages from AJV -- ✅ Performance: 100 reports validated in <1 second -- ✅ Backward compatibility with hand-coded validator -- ✅ Schema catches errors hand-coded validator misses -- ✅ All 32 type-specific schemas loaded and available -- ✅ Master schema compiles successfully -- ✅ Schema infrastructure fully implemented - -**Known Limitation:** - -- ⚠️ Type-specific required fields not enforced by master schema validation -- This is due to a design flaw in the XARF spec's master schema structure -- The master schema uses `anyOf` with `if/then` which has a logical issue: - - When `if` condition doesn't match, validation succeeds (then is not applied) - - With `anyOf`, if ANY branch succeeds, the whole anyOf succeeds - - Result: Reports always pass even when missing type-specific required fields - -**Recommended Fix (for XARF spec team):** -Restructure master schema from: - -```json -{ "anyOf": [ - { "if": { ... }, "then": { "$ref": "types/..." } } - ]} -``` - -To: - -```json -{ "oneOf": [ - { "allOf": [ - { "properties": { "category": ..., "type": ... } }, - { "$ref": "types/..." } - ]} - ]} -``` - -**Current Recommendation:** - -- Use `SchemaValidator.validateCore()` for production validation -- Core schema provides excellent coverage of XARF v4.0.0 spec -- Type-specific validation requires hand-coded validator or spec master schema fix - -## Completed - -### snake_case API (Fixed 2025-12-16, camelCase compat removed) - -- ✅ Generator uses snake_case field names matching the XARF spec -- ✅ camelCase backward compatibility removed (was never in the spec) - -## Low Priority - -- Add more comprehensive examples for all 7 categories -- Performance benchmarks -- Streaming parser for large reports diff --git a/UPGRADE_SUMMARY_v1.0.0.md b/UPGRADE_SUMMARY_v1.0.0.md deleted file mode 100644 index 26962da..0000000 --- a/UPGRADE_SUMMARY_v1.0.0.md +++ /dev/null @@ -1,206 +0,0 @@ -# XARF JavaScript v1.0.0 Upgrade Summary - -**Date**: November 30, 2025 -**Upgrade**: v1.0.0-alpha.2 → v1.0.0 (Production Release) - -## ✅ Upgrade Complete - -All tasks completed successfully. The xarf-javascript library has been upgraded to production-quality v1.0.0 following the exact same pattern as xarf-go and xarf-python. - -## 🎯 Changes Made - -### 1. **Category Correction (CRITICAL)** -- ✅ Removed "other" category from `src/types.ts` -- ✅ Updated to exactly 7 categories per XARF v4.0.0 specification: - 1. messaging - 2. connection - 3. content - 4. infrastructure - 5. copyright - 6. vulnerability - 7. reputation -- ✅ Updated `src/generator.ts` to remove 'other' from VALID_CATEGORIES -- ✅ Updated `src/validator.ts` to validate only 7 categories -- ✅ Updated `src/v3-legacy.ts` to map unknown v3 types to `content/unclassified` instead of `other/unclassified` -- ✅ Removed `OtherReport` interface and export - -### 2. **Version Updates** -- ✅ Updated `package.json` version: `1.0.0-alpha.2` → `1.0.0` -- ✅ Updated `src/index.ts` VERSION constant: `1.0.0-alpha.1` → `1.0.0` - -### 3. **Documentation Updates** -- ✅ Updated `README.md`: - - Changed "8 categories" to "7 categories" - - Removed "Other" from category list - - Updated version section to reflect production release - - Changed unknown v3 mapping from "other" to "content" -- ✅ Updated `CHANGELOG.md`: - - Added v1.0.0 release section dated 2025-11-30 - - Documented breaking change (removal of "other" category) - - Comprehensive list of changes and improvements - - Updated alpha.1 entry to reflect 7 categories -- ✅ Updated `SECURITY.md`: - - Added v1.0.0 to supported versions - - Marked alpha versions as unsupported -- ✅ Created `docs/MIGRATION_V3_TO_V4.md`: - - Moved and enhanced existing MIGRATION.md - - Added note about 7 categories and unknown type mapping - -### 4. **New Documentation** -- ✅ Created `RELEASE_NOTES_1.0.0.md`: - - Comprehensive release notes - - Feature highlights - - Breaking changes documentation - - Migration guide from alpha - - Examples for all 7 categories - - Installation and upgrade instructions - -### 5. **CI/CD Enhancements** -- ✅ Enhanced `.github/workflows/ci.yml`: - - Added Node.js 16 support (now tests: 16, 18, 20, 22) - - Added security audit job - - Added npm audit with moderate and high level checks - - Added format checking - -### 6. **Test Updates** -- ✅ Fixed all tests to use 7 categories instead of 8 -- ✅ Updated `tests/generator.test.ts`: Changed size check from 8 to 7 -- ✅ Updated `tests/generator.edge-cases.test.ts`: Removed 'other' references -- ✅ Updated `tests/parser.edge-cases.test.ts`: Changed 'other' to 'content' -- ✅ Updated `tests/v3-legacy.test.ts`: Changed expected category from 'other' to 'content' -- ✅ All 142 tests passing - -## 📊 Verification Results - -### Build Status -``` -✅ TypeScript compilation: SUCCESS -✅ No build errors -``` - -### Test Status -``` -✅ Test Suites: 8 passed, 8 total -✅ Tests: 142 passed, 142 total -✅ Coverage: 80%+ -``` - -### Security Audit -``` -✅ npm audit (production): 0 vulnerabilities -✅ No security issues found -``` - -### Code Quality -``` -✅ TypeScript type checking: PASSED -✅ Linting: PASSED (only test warnings for 'any' types, acceptable) -✅ Build successful -``` - -## 🔄 Breaking Changes - -### For Users Upgrading from Alpha - -**Critical**: The "other" category has been removed. - -**Migration Required**: -```typescript -// Before (alpha) -{ category: 'other', type: 'unclassified' } - -// After (v1.0.0) -{ category: 'content', type: 'unclassified' } -``` - -**Search Your Codebase**: -```bash -grep -r "category.*other" . -``` - -Replace all instances of `category: 'other'` with `category: 'content'`. - -## 📁 File Structure - -``` -xarf-javascript/ -├── CHANGELOG.md ✅ Updated (v1.0.0 section) -├── SECURITY.md ✅ Updated (version table) -├── README.md ✅ Updated (7 categories) -├── RELEASE_NOTES_1.0.0.md ✅ Created -├── package.json ✅ Updated (v1.0.0) -├── docs/ -│ └── MIGRATION_V3_TO_V4.md ✅ Created -├── .github/workflows/ -│ └── ci.yml ✅ Enhanced -├── src/ -│ ├── types.ts ✅ Fixed (7 categories) -│ ├── generator.ts ✅ Fixed (removed 'other') -│ ├── validator.ts ✅ Fixed (7 categories) -│ ├── v3-legacy.ts ✅ Fixed (maps to 'content') -│ └── index.ts ✅ Updated (v1.0.0, removed OtherReport) -└── tests/ - ├── *.test.ts ✅ All fixed and passing -``` - -## 🚀 Next Steps for Users - -### 1. Update Package -```bash -npm install xarf@1.0.0 -``` - -### 2. Search for Breaking Changes -```bash -grep -r "category.*'other'" . -grep -r 'category.*"other"' . -``` - -### 3. Update Code -Replace all `category: 'other'` with `category: 'content'` - -### 4. Run Tests -```bash -npm test -``` - -### 5. Verify Build -```bash -npm run build -``` - -## 📚 Documentation References - -- **README.md**: Quick start and API overview -- **RELEASE_NOTES_1.0.0.md**: Complete v1.0.0 release information -- **docs/MIGRATION_V3_TO_V4.md**: V3 to V4 migration guide -- **CHANGELOG.md**: Complete version history -- **SECURITY.md**: Security policy - -## ✨ Features - -All 7 XARF v4.0.0 categories fully supported: -- ✅ Messaging (spam, phishing, social_engineering, bulk_messaging) -- ✅ Connection (ddos, port_scan, login_attack, etc.) -- ✅ Content (phishing_site, malware_distribution, defacement, etc.) -- ✅ Infrastructure (botnet, compromised_server) -- ✅ Copyright (infringement, dmca, trademark, etc.) -- ✅ Vulnerability (cve, misconfiguration, open_service) -- ✅ Reputation (blocklist, threat_intelligence) - -## 🎉 Success Criteria Met - -- ✅ Only 7 categories in code (matches specification exactly) -- ✅ Version is v1.0.0 -- ✅ V3 compatibility verified working -- ✅ All documentation created (CHANGELOG, SECURITY, MIGRATION, RELEASE_NOTES) -- ✅ README updated -- ✅ All 142 tests passing -- ✅ Build successful -- ✅ No security vulnerabilities -- ✅ CI/CD enhanced with Node 16-22 support -- ✅ Follows exact same pattern as xarf-go and xarf-python - ---- - -**Upgrade completed successfully!** 🎊 diff --git a/docs/FIELD-NAMING.md b/docs/FIELD-NAMING.md deleted file mode 100644 index 7c52f60..0000000 --- a/docs/FIELD-NAMING.md +++ /dev/null @@ -1,53 +0,0 @@ -# Field Naming Conventions - -## snake_case Throughout - -The XARF v4.0.0 specification uses **snake_case** for all field names, and this library follows that convention in both input and output. - -### Example - -```typescript -import { XARFGenerator } from '@xarf/xarf-javascript'; - -const generator = new XARFGenerator(); - -const report = generator.generateReport({ - category: 'connection', - type: 'ddos', - source_identifier: '192.0.2.100', - evidence_source: 'honeypot', - reporter: { - org: 'Security Team', - contact: 'security@example.com', - domain: 'example.com', - }, - sender: { - org: 'SOC', - contact: 'soc@example.com', - domain: 'example.com', - }, - destination_ip: '203.0.113.50', - protocol: 'tcp', -}); -``` - -### Additional Fields - -The `additionalFields` object should also use snake_case field names: - -```typescript -generator.generateReport({ - // ... - additionalFields: { - destination_ip: '203.0.113.50', - destination_port: 80, - packet_count: 1500, - }, -}); -``` - -## See Also - -- [XARF v4.0.0 Specification](https://xarf.org/spec/) -- [Generator Examples](../examples/snake-case-usage.ts) -- [API Reference](./API.md) diff --git a/docs/MIGRATION_V3_TO_V4.md b/docs/MIGRATION_V3_TO_V4.md index c0deaf0..8496d1a 100644 --- a/docs/MIGRATION_V3_TO_V4.md +++ b/docs/MIGRATION_V3_TO_V4.md @@ -9,12 +9,14 @@ XARF v4 introduces a category-based architecture that improves upon the v3 forma The library automatically detects and converts v3 reports to v4 format: ```typescript -import { XARFParser } from 'xarf'; - -const parser = new XARFParser(); - -// v3 report is automatically converted -const report = parser.parse(v3JsonData); +import { parse } from 'xarf'; + +// v3 report is automatically detected and converted +const { report, warnings } = parse(v3JsonData); +// warnings includes: +// "DEPRECATION WARNING: XARF v3 format detected. The v3 format has been +// automatically converted to v4. Please update your systems to generate +// v4 reports directly. v3 support will be removed in a future major version." ``` ## What Changes @@ -42,20 +44,24 @@ const report = parser.parse(v3JsonData); ```json { - "xarf_version": "4.0.0", + "xarf_version": "4.2.0", "report_id": "auto-generated-uuid", "timestamp": "2024-01-15T10:00:00Z", "reporter": { "org": "Security Team", "contact": "abuse@example.com", - "type": "manual" + "domain": "example.com" + }, + "sender": { + "org": "Security Team", + "contact": "abuse@example.com", + "domain": "example.com" }, "source_identifier": "192.0.2.1", "category": "messaging", "type": "spam", - "evidence_source": "manual_analysis", + "legacy_version": "3", "_internal": { - "legacy_version": "3", "original_report_type": "Spam", "converted_at": "2024-01-15T10:05:00Z" } @@ -64,18 +70,19 @@ const report = parser.parse(v3JsonData); ### Field Mappings -| v3 Field | v4 Field | Notes | -| --------------------------------------- | ------------------- | --------------------------- | -| `Version` | `xarf_version` | Set to "4.0.0" | -| N/A | `report_id` | Auto-generated UUID | -| `ReporterInfo.ReporterOrg` | `reporter.org` | Direct mapping | -| `ReporterInfo.ReporterOrgEmail` | `reporter.contact` | Direct mapping | -| N/A | `reporter.type` | Set to "manual" for v3 | -| `Report.Date` | `timestamp` | Direct mapping | -| `Report.SourceIp` or `Report.Source.IP` | `source_identifier` | Uses Source.IP if available | -| `Report.ReportType` | `category` + `type` | Mapped per table below | -| `Report.Attachment` or `Report.Samples` | `evidence` | Structure converted | -| N/A | `evidence_source` | Default: "manual_analysis" | +| v3 Field | v4 Field | Notes | +| --------------------------------------- | ------------------- | ------------------------------------------------- | +| `Version` | `xarf_version` | Set to "4.2.0" | +| N/A | `report_id` | Auto-generated UUID | +| `ReporterInfo.ReporterOrg` | `reporter.org` | Direct mapping | +| `ReporterInfo.ReporterOrgEmail` | `reporter.contact` | Direct mapping | +| `ReporterInfo.ReporterOrgEmail` | `reporter.domain` | Extracted from email domain part | +| N/A | `sender` | Set to same values as `reporter` | +| `Report.Date` | `timestamp` | Direct mapping | +| `Report.SourceIp` or `Report.Source.IP` | `source_identifier` | Priority: Source.IP > SourceIp > Source.URL > Url | +| `Report.ReportType` | `category` + `type` | Mapped per table below | +| `Report.Attachment` or `Report.Samples` | `evidence` | Structure converted, hash and size added | +| `Report.AdditionalInfo.DetectionMethod` | `evidence_source` | Only set if explicitly provided in v3 | ### Report Type Mappings @@ -89,19 +96,18 @@ const report = parser.parse(v3JsonData); | `Malware` | `content` | `malware` | | `Botnet` | `infrastructure` | `botnet` | | `Copyright` | `copyright` | `copyright` | -| Unknown types | `content` | `unclassified` | -**Note**: XARF v4 has exactly 7 categories. Unknown v3 report types are mapped to the `content` category with type `unclassified`. +**Note**: Unknown v3 report types are not silently converted — they cause a parse error listing the supported types. Only the 8 types above are supported. ## Deprecation Warnings When parsing v3 reports, you'll receive deprecation warnings: ```typescript -const parser = new XARFParser(); -const report = parser.parse(v3Report); +import { parse } from 'xarf'; -const warnings = parser.getWarnings(); +const { report, warnings } = parse(v3Report); +// warnings includes: // [ // "DEPRECATION WARNING: XARF v3 format detected. The v3 format has been automatically converted to v4. Please update your systems to generate v4 reports directly. v3 support will be removed in a future major version.", // ...conversion warnings... @@ -115,12 +121,12 @@ const warnings = parser.getWarnings(); Use the library's automatic conversion: ```typescript -const parser = new XARFParser(); +import { parse } from 'xarf'; -function processReport(jsonData: unknown) { - const report = parser.parse(jsonData); +function processReport(jsonData: string | Record) { + const { report } = parse(jsonData); - if (report._internal?.legacy_version === '3') { + if (report.legacy_version === '3') { console.log('Received v3 report - consider upgrading sender'); } @@ -134,11 +140,12 @@ function processReport(jsonData: unknown) { Track v3 report usage to plan deprecation: ```typescript -function trackLegacyUsage(jsonData: unknown) { - const parser = new XARFParser(); - const report = parser.parse(jsonData); +import { parse } from 'xarf'; - if (report._internal?.legacy_version === '3') { +function trackLegacyUsage(jsonData: string | Record) { + const { report } = parse(jsonData); + + if (report.legacy_version === '3') { metrics.increment('xarf.v3.reports'); logDeprecationNotice(report.reporter.contact); } @@ -150,11 +157,9 @@ function trackLegacyUsage(jsonData: unknown) { Update your report generators to produce v4 format: ```typescript -import { XARFGenerator } from 'xarf'; - -const generator = new XARFGenerator(); +import { createReport } from 'xarf'; -const report = generator.generateReport({ +const { report } = createReport({ category: 'messaging', type: 'spam', source_identifier: '192.0.2.100', @@ -172,40 +177,15 @@ const report = generator.generateReport({ }); ``` -## Testing Migration - -Test your v3 reports with the converter: - -```typescript -import { convertV3toV4, isXARFv3 } from 'xarf'; - -describe('v3 Migration', () => { - it('should convert our v3 reports', () => { - const v3Report = loadLegacyReport(); - - expect(isXARFv3(v3Report)).toBe(true); - - const warnings: string[] = []; - const v4Report = convertV3toV4(v3Report, warnings); - - expect(v4Report.xarf_version).toBe('4.0.0'); - expect(v4Report.category).toBeDefined(); - expect(v4Report.type).toBeDefined(); - - // Review any conversion warnings - warnings.forEach((warning) => console.log(warning)); - }); -}); -``` - ## Breaking Changes from v3 1. **Required Fields**: v4 requires `report_id` (UUID) - auto-generated during conversion -2. **Reporter Type**: v4 requires `reporter.type` - defaults to "manual" for v3 conversions -3. **Evidence Source**: v4 requires `evidence_source` - defaults to "manual_analysis" for v3 +2. **Reporter Domain**: v4 requires `reporter.domain` - extracted from the reporter email address +3. **Sender Field**: v4 requires a `sender` object - set to the same values as `reporter` during conversion 4. **Category System**: v3's single `ReportType` becomes `category` + `type` in v4 5. **Timestamp Format**: Both use ISO 8601, but v4 is more strict -6. **Evidence Structure**: v3's `Attachment`/`Samples` becomes structured `evidence` array +6. **Evidence Structure**: v3's `Attachment`/`Samples` becomes structured `evidence` array with computed `hash` (SHA256) and `size` fields +7. **Evidence Source**: v4's `evidence_source` is only set if `AdditionalInfo.DetectionMethod` is present in the v3 report — it is not defaulted ## Unsupported v3 Features @@ -231,10 +211,3 @@ v4Report._internal = { - Check the [XARF v4 Specification](https://xarf.org) - Review [API Documentation](https://github.com/xarf/xarf-javascript) - Open an [Issue](https://github.com/xarf/xarf-javascript/issues) - -## Timeline - -- **Phase 1 (Current)**: Full v3 support with automatic conversion -- **Phase 2 (6 months)**: v3 support maintained, deprecation warnings -- **Phase 3 (12 months)**: Advanced notice of v3 support removal -- **Phase 4 (18 months)**: v3 support removed in next major version diff --git a/examples/schema-validation.ts b/examples/schema-validation.ts deleted file mode 100644 index 546e632..0000000 --- a/examples/schema-validation.ts +++ /dev/null @@ -1,288 +0,0 @@ -/** - * Schema Validation Examples - * - * Demonstrates how to use the SchemaValidator to validate XARF reports - * against JSON schemas with full type-specific validation. - */ - -import { validator, SchemaValidator, type XARFReport } from '../src/index'; - -// Example 1: Using the singleton validator instance -function exampleSingletonValidator() { - console.log('\n=== Example 1: Singleton Validator ===\n'); - - const report: XARFReport = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T14:30:25Z', - reporter: { - org: 'Example Security', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Example Security', - contact: 'abuse@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'messaging', - type: 'spam', - evidence_source: 'spamtrap', - }; - - const result = validator.validate(report); - - if (result.valid) { - console.log('✓ Report is valid'); - } else { - console.log('✗ Validation failed:'); - result.errors.forEach((error) => console.log(` - ${error}`)); - } -} - -// Example 2: Creating a custom validator instance -function exampleCustomValidator() { - console.log('\n=== Example 2: Custom Validator Instance ===\n'); - - const customValidator = new SchemaValidator(); - - const report: XARFReport = { - xarf_version: '4.0.0', - report_id: 'invalid-uuid', // This will fail validation - timestamp: '2024-01-15T14:30:25Z', - reporter: { - org: 'Example Security', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Example Security', - contact: 'abuse@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'messaging', - type: 'spam', - evidence_source: 'spamtrap', - }; - - const result = customValidator.validate(report); - - if (!result.valid) { - console.log('✗ Expected validation errors:'); - result.errors.forEach((error) => console.log(` - ${error}`)); - } -} - -// Example 3: Validating type-specific fields -function exampleTypeSpecificValidation() { - console.log('\n=== Example 3: Type-Specific Validation ===\n'); - - // Messaging spam report with protocol-specific fields - const spamReport: XARFReport = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440001', - timestamp: '2024-01-15T14:30:25Z', - reporter: { - org: 'SpamCop', - contact: 'reports@spamcop.net', - domain: 'spamcop.net', - }, - sender: { - org: 'SpamCop', - contact: 'reports@spamcop.net', - domain: 'spamcop.net', - }, - source_identifier: '192.0.2.123', - source_port: 25, // Required for SMTP spam reports - category: 'messaging', - type: 'spam', - protocol: 'smtp', - smtp_from: 'spammer@example.com', - smtp_to: 'victim@example.org', - subject: 'Urgent: Verify Your Account', - evidence_source: 'spamtrap', - }; - - const result = validator.validate(spamReport); - - if (result.valid) { - console.log('✓ Spam report with type-specific fields is valid'); - } else { - console.log('✗ Validation failed:'); - result.errors.forEach((error) => console.log(` - ${error}`)); - } -} - -// Example 4: Core-only validation (without type-specific checks) -function exampleCoreOnlyValidation() { - console.log('\n=== Example 4: Core-Only Validation ===\n'); - - const report: XARFReport = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440002', - timestamp: '2024-01-15T14:30:25Z', - reporter: { - org: 'Example Security', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Example Security', - contact: 'abuse@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'messaging', - type: 'spam', - evidence_source: 'spamtrap', - }; - - const result = validator.validateCore(report); - - if (result.valid) { - console.log('✓ Core validation passed (type-specific validation not performed)'); - } else { - console.log('✗ Core validation failed:'); - result.errors.forEach((error) => console.log(` - ${error}`)); - } -} - -// Example 5: Checking supported types -function exampleSupportedTypes() { - console.log('\n=== Example 5: Supported Types ===\n'); - - const types = validator.getSupportedTypes(); - console.log(`Found ${types.length} supported category+type combinations:`); - - // Group by category - const byCategory: Record = {}; - types.forEach(({ category, type }) => { - if (!byCategory[category]) { - byCategory[category] = []; - } - byCategory[category].push(type); - }); - - Object.keys(byCategory) - .sort() - .forEach((category) => { - console.log(`\n${category}:`); - byCategory[category].forEach((type) => { - console.log(` - ${type}`); - }); - }); -} - -// Example 6: Checking if a specific type is supported -function exampleTypeSupport() { - console.log('\n=== Example 6: Type Support Check ===\n'); - - const checks = [ - { category: 'messaging', type: 'spam' }, - { category: 'connection', type: 'ddos' }, - { category: 'content', type: 'phishing' }, - { category: 'messaging', type: 'nonexistent' }, - ]; - - checks.forEach(({ category, type }) => { - const supported = validator.hasTypeSchema(category, type); - const status = supported ? '✓' : '✗'; - console.log(`${status} ${category}/${type}: ${supported ? 'supported' : 'not supported'}`); - }); -} - -// Example 7: Handling validation errors gracefully -function exampleErrorHandling() { - console.log('\n=== Example 7: Error Handling ===\n'); - - // Missing required fields - const invalidReport = { - xarf_version: '4.0.0', - // Missing report_id, timestamp, etc. - category: 'messaging', - type: 'spam', - } as XARFReport; - - const result = validator.validate(invalidReport); - - if (!result.valid) { - console.log('✗ Validation errors found:'); - result.errors.forEach((error, index) => { - console.log(` ${index + 1}. ${error}`); - }); - } -} - -// Example 8: Full validation with evidence -function exampleWithEvidence() { - console.log('\n=== Example 8: Validation with Evidence ===\n'); - - const report: XARFReport = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440003', - timestamp: '2024-01-15T14:30:25Z', - reporter: { - org: 'Security Research Lab', - contact: 'reports@seclab.org', - domain: 'seclab.org', - }, - sender: { - org: 'Security Research Lab', - contact: 'reports@seclab.org', - domain: 'seclab.org', - }, - source_identifier: '198.51.100.42', - category: 'connection', - type: 'ddos', - evidence_source: 'ids_ips', - evidence: [ - { - content_type: 'text/plain', - description: 'Network flow analysis logs', - payload: 'VGhpcyBpcyBhIGJhc2U2NC1lbmNvZGVkIHBheWxvYWQ=', - hash: 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', - }, - ], - tags: ['attack:syn_flood', 'volume:high'], - confidence: 0.95, - }; - - const result = validator.validate(report); - - if (result.valid) { - console.log('✓ Report with evidence validated successfully'); - } else { - console.log('✗ Validation failed:'); - result.errors.forEach((error) => console.log(` - ${error}`)); - } -} - -// Run all examples -function runAllExamples() { - console.log('╔═══════════════════════════════════════════════════════════════╗'); - console.log('║ XARF Schema Validator - Usage Examples ║'); - console.log('╚═══════════════════════════════════════════════════════════════╝'); - - try { - exampleSingletonValidator(); - exampleCustomValidator(); - exampleTypeSpecificValidation(); - exampleCoreOnlyValidation(); - exampleSupportedTypes(); - exampleTypeSupport(); - exampleErrorHandling(); - exampleWithEvidence(); - - console.log('\n✓ All examples completed\n'); - } catch (error) { - console.error('\n✗ Error running examples:', error); - process.exit(1); - } -} - -// Run examples if this file is executed directly -if (require.main === module) { - runAllExamples(); -} diff --git a/examples/snake-case-usage.ts b/examples/snake-case-usage.ts deleted file mode 100644 index 6e38e25..0000000 --- a/examples/snake-case-usage.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Example: Using snake_case field names (XARF spec format) - * - * This example demonstrates using the XARF spec's snake_case field names - * when generating reports. This is the preferred approach as it matches - * the official XARF specification. - */ - -import { XARFGenerator } from '../src/generator'; - -const generator = new XARFGenerator(); - -// Example 1: Basic report using snake_case (XARF spec format) -console.log('Example 1: Basic connection report with snake_case'); -const report1 = generator.generateReport({ - category: 'connection', - type: 'ddos', // XARF spec field name (not reportType) - source_identifier: '192.0.2.100', // XARF spec field name (not sourceIdentifier) - evidence_source: 'honeypot', // XARF spec field name (not evidenceSource) - reporter: { - org: 'Security Operations Center', - contact: 'abuse@security.example.com', - domain: 'security.example.com', - }, - sender: { - org: 'SOC Automated Systems', - contact: 'reports@security.example.com', - domain: 'security.example.com', - }, - description: 'DDoS attack detected from monitoring systems', - severity: 'high', - confidence: 0.95, - additionalFields: { - destination_ip: '203.0.113.50', - protocol: 'tcp', - destination_port: 80, - packet_count: 150000, - }, -}); - -console.log(JSON.stringify(report1, null, 2)); -console.log('\n---\n'); - -// Example 2: Report with on_behalf_of using snake_case -console.log('Example 2: Messaging report with on_behalf_of (snake_case)'); -const report2 = generator.generateReport({ - category: 'messaging', - type: 'phishing', // XARF spec - source_identifier: '198.51.100.25', // XARF spec - evidence_source: 'user_report', // XARF spec - on_behalf_of: { - // XARF spec (not onBehalfOf) - org: 'Client Corporation', - contact: 'abuse@client.example.com', - domain: 'client.example.com', - }, - reporter: { - org: 'Abuse Response Team', - contact: 'abuse@provider.example.com', - domain: 'provider.example.com', - }, - sender: { - org: 'Email Security Division', - contact: 'phishing@provider.example.com', - domain: 'provider.example.com', - }, - description: 'Phishing email reported by end user', - severity: 'medium', - confidence: 0.85, - additionalFields: { - protocol: 'smtp', - smtp_from: 'fake-support@evil.example.com', - smtp_to: 'victim@client.example.com', - subject: 'Urgent: Verify Your Account', - message_id: '<1234567890@evil.example.com>', - }, - evidence: [ - generator.addEvidence( - 'message/rfc822', - 'Original phishing email message', - 'From: fake-support@evil.example.com\nSubject: Urgent: Verify Your Account\n\nClick here to verify...' - ), - ], -}); - -console.log(JSON.stringify(report2, null, 2)); -console.log('\n---\n'); - -console.log('Note: All field names use snake_case, matching the XARF specification.'); diff --git a/package-lock.json b/package-lock.json index 2943164..8c86301 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,8 @@ "devDependencies": { "@types/jest": "^29.5.11", "@types/node": "^25.0.3", - "@typescript-eslint/eslint-plugin": "^6.17.0", - "@typescript-eslint/parser": "^6.17.0", + "@typescript-eslint/eslint-plugin": "^8.57.0", + "@typescript-eslint/parser": "^8.57.0", "eslint": "^8.56.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-complexity": "^1.0.2", @@ -68,7 +68,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -571,20 +570,6 @@ "node": ">=20.11.0" } }, - "node_modules/@es-joy/jsdoccomment/node_modules/@typescript-eslint/types": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", - "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@es-joy/resolve.exports": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@es-joy/resolve.exports/-/resolve.exports-1.2.0.tgz", @@ -649,9 +634,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -684,9 +669,9 @@ "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -734,9 +719,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -768,6 +753,29 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -1356,9 +1364,9 @@ } }, "node_modules/@ts-morph/common/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1468,13 +1476,6 @@ "pretty-format": "^29.0.0" } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { "version": "25.0.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", @@ -1492,13 +1493,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -1524,125 +1518,159 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", + "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/type-utils": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^2.4.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@typescript-eslint/parser": "^8.57.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", + "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", "dev": true, - "license": "BSD-2-Clause", - "peer": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", + "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.0", + "@typescript-eslint/types": "^8.57.0", + "debug": "^4.4.3" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", + "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", + "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", + "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", + "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", "dev": true, "license": "MIT", "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1650,78 +1678,88 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", + "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "@typescript-eslint/project-service": "8.57.0", + "@typescript-eslint/tsconfig-utils": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", + "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", + "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "8.57.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -1735,7 +1773,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1754,9 +1791,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -1872,16 +1909,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -2025,16 +2052,6 @@ "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -2068,7 +2085,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2551,19 +2567,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2657,7 +2660,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2714,7 +2716,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -2827,66 +2828,43 @@ } }, "node_modules/eslint-plugin-sonarjs": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-3.0.5.tgz", - "integrity": "sha512-dI62Ff3zMezUToi161hs2i1HX1ie8Ia2hO0jtNBfdgRBicAG4ydy2WPt0rMTrAe3ZrlqhpAO3w1jcQEdneYoFA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-3.0.7.tgz", + "integrity": "sha512-62jB20krIPvcwBLAyG3VVKa2ce2j2lL1yCb8Y0ylMRR/dLvCCTiQx8gQbXb+G81k1alPZ2/I3muZinqWQdBbzw==", "dev": true, "license": "LGPL-3.0-only", "dependencies": { - "@eslint-community/regexpp": "4.12.1", + "@eslint-community/regexpp": "4.12.2", "builtin-modules": "3.3.0", "bytes": "3.1.2", "functional-red-black-tree": "1.0.1", "jsx-ast-utils-x": "0.1.0", "lodash.merge": "4.6.2", - "minimatch": "9.0.5", + "minimatch": "10.1.2", "scslre": "0.3.0", - "semver": "7.7.2", + "semver": "7.7.4", "typescript": ">=5" }, "peerDependencies": { "eslint": "^8.0.0 || ^9.0.0" } }, - "node_modules/eslint-plugin-sonarjs/node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, "node_modules/eslint-plugin-sonarjs/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.1" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/eslint-plugin-sonarjs/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -2947,9 +2925,9 @@ } }, "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -2982,9 +2960,9 @@ "license": "MIT" }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3281,9 +3259,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -3429,9 +3407,9 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3457,27 +3435,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3846,7 +3803,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -4713,9 +4669,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, @@ -4952,21 +4908,44 @@ } }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimatch/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/minimatch/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -5408,7 +5387,6 @@ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -5773,9 +5751,9 @@ } }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -6082,9 +6060,9 @@ } }, "node_modules/tar": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", - "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", + "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -6135,9 +6113,9 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -6154,6 +6132,54 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -6202,16 +6228,16 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/ts-jest": { @@ -6361,7 +6387,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index bc87e35..8364765 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "xarf", "version": "1.0.0", - "description": "XARF v4 (eXtended Abuse Reporting Format) parser and generator for JavaScript/TypeScript - supports XARF spec v4.0.0 with backward compatibility for v3", + "description": "XARF v4 (eXtended Abuse Reporting Format) parser and generator for JavaScript/TypeScript - supports XARF spec v4.2.0 with backward compatibility for v3", "xarfSpec": { - "version": "v4.1.0", + "version": "v4.2.0", "repository": "https://github.com/xarf/xarf-spec" }, "main": "dist/index.js", @@ -64,8 +64,8 @@ "devDependencies": { "@types/jest": "^29.5.11", "@types/node": "^25.0.3", - "@typescript-eslint/eslint-plugin": "^6.17.0", - "@typescript-eslint/parser": "^6.17.0", + "@typescript-eslint/eslint-plugin": "^8.57.0", + "@typescript-eslint/parser": "^8.57.0", "eslint": "^8.56.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-complexity": "^1.0.2", diff --git a/src/errors.ts b/src/errors.ts index 8adf473..8b10f82 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -50,18 +50,3 @@ export class XARFParseError extends XARFError { Object.setPrototypeOf(this, XARFParseError.prototype); } } - -/** - * Error thrown when XARF schema validation fails - */ -export class XARFSchemaError extends XARFError { - /** - * Create a new XARF schema error - * @param message - Error message - */ - constructor(message: string) { - super(message); - this.name = 'XARFSchemaError'; - Object.setPrototypeOf(this, XARFSchemaError.prototype); - } -} diff --git a/src/generator.ts b/src/generator.ts index 62739da..8c995cd 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -1,833 +1,172 @@ /** * XARF Report Generator * - * This module provides functionality for generating XARF v4.0.0 compliant reports - * programmatically with proper validation and type safety. + * Generates XARF v4.2.0 compliant reports with automatic metadata, + * validation, and type safety derived from parser types. */ -import { randomBytes, randomUUID, createHash } from 'crypto'; -import { XARFError } from './errors'; -import { schemaRegistry } from './schema-registry'; +import { createHash, randomUUID } from 'crypto'; import { XARFValidator } from './validator'; -import { validateContactInfo as validateContactInfoUtil } from './validation-utils'; import type { - XARFReport, - XARFCategory, - ReporterType, - EvidenceSource, - SeverityLevel, XARFEvidence, + XARFReport, + ConnectionReport, + MessagingReport, + ContentReport, + InfrastructureReport, + CopyrightReport, + VulnerabilityReport, + ReputationReport, } from './types'; +import type { ValidationError, ValidationWarning, ValidationInfo } from './validator'; +import { SPEC_VERSION } from './version'; /** - * Base generator options shared by all categories + * A bit of Typescprit magic to derive the generator options from a report + * type (e.g. ConnectionReport → ConnectionReportInput). + * + * The report interfaces have an index signature ([key: string]: unknown) which causes + * Omit to collapse to just the index signature, losing all named properties. + * RemoveIndex strips it first so Omit can work on the actual fields, then we: + * 1. Remove xarf_version, report_id, timestamp (auto-filled by the generator) + * 2. Add report_id and timestamp back as optional overrides + * 3. Re-add the index signature for arbitrary spec fields */ -export interface BaseGeneratorOptions { - type?: string; - source_identifier?: string; - - reporter: { - org: string; - contact: string; - domain: string; - }; - sender: { - org: string; - contact: string; - domain: string; - }; - - evidence_source?: EvidenceSource; - - description?: string; - evidence?: XARFEvidence[]; - confidence?: number; - tags?: string[]; - - additionalFields?: Record; -} +type RemoveIndex = { + [K in keyof T as string extends K ? never : K]: T[K]; +}; +type MakeReportInput = Omit< + RemoveIndex, + 'xarf_version' | 'report_id' | 'timestamp' +> & { report_id?: string; timestamp?: string; [key: string]: unknown }; /** - * Content category options with required url field + * Category-specific report input types, derived from the corresponding report types. */ -export interface ContentGeneratorOptions extends BaseGeneratorOptions { - category: 'content'; - /** URL of the malicious content (required for content reports) */ - url?: string; - /** Fully qualified domain name */ - domain?: string; - /** Domain registrar */ - registrar?: string; - /** DNS nameservers */ - nameservers?: string[]; - /** Screenshot URL */ - screenshot_url?: string; - /** When content was verified */ - verified_at?: string; - /** How content was verified */ - verification_method?: string; - /** Primary attack vector */ - attack_vector?: string; - /** Impersonated brand */ - target_brand?: string; - /** Hosting provider */ - hosting_provider?: string; - /** Autonomous System Number */ - asn?: number; - /** ISO country code */ - country_code?: string; - /** Allow any additional content fields from schema */ - [key: string]: unknown; -} +export type ConnectionReportInput = MakeReportInput; +export type MessagingReportInput = MakeReportInput; +export type ContentReportInput = MakeReportInput; +export type InfrastructureReportInput = MakeReportInput; +export type CopyrightReportInput = MakeReportInput; +export type VulnerabilityReportInput = MakeReportInput; +export type ReputationReportInput = MakeReportInput; /** - * Connection category options with required destination_ip and protocol fields + * Discriminated union of all category-specific report inputs. + * Narrows on `category` to provide autocomplete for category-specific fields. */ -export interface ConnectionGeneratorOptions extends BaseGeneratorOptions { - category: 'connection'; - /** Destination IP address (required for connection reports) */ - destination_ip?: string; - /** Network protocol (required for connection reports) */ - protocol?: string; - /** Source port number (required when source_identifier is an IP) */ - source_port?: number; - /** First seen timestamp (required for connection types) */ - first_seen?: string; - /** Destination port number */ - destination_port?: number; - /** Attack vector type */ - attack_vector?: string; - /** Peak packets per second */ - peak_pps?: number; - /** Peak bits per second */ - peak_bps?: number; - /** Duration in seconds */ - duration_seconds?: number; - /** Allow any additional connection fields from schema */ - [key: string]: unknown; -} +export type ReportInput = + | ConnectionReportInput + | MessagingReportInput + | ContentReportInput + | InfrastructureReportInput + | CopyrightReportInput + | VulnerabilityReportInput + | ReputationReportInput; /** - * Messaging category options with protocol-specific fields + * Options for createReport() */ -export interface MessagingGeneratorOptions extends BaseGeneratorOptions { - category: 'messaging'; - /** Messaging protocol (smtp, sms, etc.) */ - protocol?: string; - /** Source port number (required when protocol is smtp) */ - source_port?: number; - /** SMTP envelope from address */ - smtp_from?: string; - /** SMTP envelope to address */ - smtp_to?: string; - /** Email subject line */ - subject?: string; - /** Message-ID header */ - message_id?: string; - /** Sender display name */ - sender_name?: string; - /** Allow any additional messaging fields from schema */ - [key: string]: unknown; +export interface CreateReportOptions { + strict?: boolean; + showMissingOptional?: boolean; } /** - * Infrastructure category options + * Result of createReport() */ -export interface InfrastructureGeneratorOptions extends BaseGeneratorOptions { - category: 'infrastructure'; - /** Command and control server details */ - c2_server?: string; - /** Malware family name */ - malware_family?: string; - /** Botnet name */ - botnet_name?: string; - /** Allow any additional infrastructure fields from schema */ - [key: string]: unknown; +export interface CreateReportResult { + report: XARFReport; + errors: ValidationError[]; + warnings: ValidationWarning[]; + info?: ValidationInfo[]; } /** - * Copyright category options + * Options for createEvidence() */ -export interface CopyrightGeneratorOptions extends BaseGeneratorOptions { - category: 'copyright'; - /** URL of infringing content */ - url?: string; - /** Title of copyrighted work */ - title?: string; - /** Copyright holder information */ - copyright_holder?: string; - /** Allow any additional copyright fields from schema */ - [key: string]: unknown; +export interface EvidenceOptions { + description?: string; + hashAlgorithm?: 'sha256' | 'sha512' | 'sha1' | 'md5'; } -/** - * Vulnerability category options - */ -export interface VulnerabilityGeneratorOptions extends BaseGeneratorOptions { - category: 'vulnerability'; - /** CVE identifier */ - cve_id?: string; - /** Affected service or software */ - service?: string; - /** Port number of vulnerable service */ - port?: number; - /** Allow any additional vulnerability fields from schema */ - [key: string]: unknown; -} +const validator = new XARFValidator(); /** - * Reputation category options + * Generate a hash of the given data. + * @param data + * @param algorithm */ -export interface ReputationGeneratorOptions extends BaseGeneratorOptions { - category: 'reputation'; - /** Name of blocklist */ - blocklist_name?: string; - /** Threat type classification */ - threat_type?: string; - /** Allow any additional reputation fields from schema */ - [key: string]: unknown; +function generateHash( + data: string | Buffer, + algorithm: 'sha256' | 'sha512' | 'sha1' | 'md5' = 'sha256' +): string { + const buffer = typeof data === 'string' ? Buffer.from(data, 'utf8') : data; + return createHash(algorithm).update(buffer).digest('hex'); } /** - * Discriminated union type for all generator options - * - * Provides type-safe, category-specific fields for each report category. - */ -export type GeneratorOptions = - | ContentGeneratorOptions - | ConnectionGeneratorOptions - | MessagingGeneratorOptions - | InfrastructureGeneratorOptions - | CopyrightGeneratorOptions - | VulnerabilityGeneratorOptions - | ReputationGeneratorOptions; - -/** - * Generator for creating XARF v4.0.0 compliant reports + * Create a validated XARF report with auto-generated metadata fields. * - * This class provides methods to generate complete XARF reports with all - * required fields, proper validation, and support for all 7 report categories. + * Auto-generates `xarf_version`, `report_id`, and `timestamp` if not provided. + * Validation runs internally — errors and warnings are returned alongside the report. + * @param input - Report data (report_id and timestamp auto-generated if omitted) + * @param options - Options controlling validation behavior + * @returns Result with report, errors, and warnings */ -export class XARFGenerator { - // XARF v4.0.0 specification constants - static readonly XARF_VERSION = '4.0.0'; - - /** - * Valid categories as per XARF spec (from schema registry) - * @returns Set of valid XARF categories - */ - static get VALID_CATEGORIES(): Set { - return schemaRegistry.getCategories(); - } - - /** - * Valid types per category (from schema registry) - * @returns Record mapping categories to their valid types - */ - static get EVENT_TYPES(): Record { - const result: Record = {}; - const allTypes = schemaRegistry.getAllTypes(); - for (const [category, types] of allTypes) { - result[category] = Array.from(types); - } - return result; - } - - /** - * Valid evidence sources (from schema registry) - * @returns Set of valid evidence sources - */ - static get VALID_EVIDENCE_SOURCES(): Set { - return schemaRegistry.getEvidenceSources(); - } - - // Valid reporter types - static readonly VALID_REPORTER_TYPES = new Set(['automated', 'manual', 'hybrid']); - - /** - * Valid severity levels (from schema registry) - * @returns Set of valid severity levels - */ - static get VALID_SEVERITIES(): Set { - return schemaRegistry.getSeverities(); - } - - // Evidence content types by category - static readonly EVIDENCE_CONTENT_TYPES: Record = { - messaging: ['message/rfc822', 'text/plain', 'text/html'], - connection: ['application/pcap', 'text/plain', 'application/json'], - content: ['image/png', 'text/html', 'application/pdf'], - infrastructure: ['application/pcap', 'text/plain', 'application/json'], - copyright: ['text/html', 'image/png', 'application/pdf'], - vulnerability: ['text/plain', 'application/json', 'image/png'], - reputation: ['application/json', 'text/plain', 'text/csv'], - other: ['text/plain', 'application/json'], +export function createReport( + input: ReportInput, + options?: CreateReportOptions +): CreateReportResult { + const strict = options?.strict ?? false; + const showMissingOptional = options?.showMissingOptional ?? false; + + const report = { + ...input, + xarf_version: SPEC_VERSION, + report_id: input.report_id ?? randomUUID(), + timestamp: input.timestamp ?? new Date().toISOString(), + } as XARFReport; + + const result = validator.validate(report, strict, showMissingOptional); + + const createReportResult: CreateReportResult = { + report, + errors: result.errors, + warnings: result.warnings, }; - /** - * Generate a UUID v4 for report identification - * @returns A string representation of a UUID v4 - */ - generateUUID(): string { - return randomUUID(); - } - - /** - * Generate an ISO 8601 formatted timestamp with UTC timezone - * @returns ISO 8601 formatted timestamp string with UTC timezone - */ - generateTimestamp(): string { - return new Date().toISOString(); - } - - /** - * Generate a cryptographic hash of the provided data - * @param data - The data to hash (string or buffer) - * @param algorithm - Hash algorithm to use (default: "sha256") - * @returns Hexadecimal string representation of the hash - * @throws {XARFError} If the algorithm is not supported - */ - generateHash( - data: string | Buffer, - algorithm: 'sha256' | 'sha512' | 'sha1' | 'md5' = 'sha256' - ): string { - const validAlgorithms = new Set(['sha256', 'sha512', 'sha1', 'md5']); - if (!validAlgorithms.has(algorithm)) { - throw new XARFError(`Unsupported hash algorithm: ${algorithm}`); - } - - const buffer = typeof data === 'string' ? Buffer.from(data, 'utf8') : data; - return createHash(algorithm).update(buffer).digest('hex'); - } - - /** - * Create an evidence item with automatic hashing - * @param contentType - MIME type of the evidence - * @param description - Human-readable description of the evidence - * @param payload - The evidence data - * @param hashAlgorithm - Algorithm to use for hashing (default: "sha256") - * @returns Evidence object with computed hash - */ - addEvidence( - contentType: string, - description: string, - payload: string | Buffer, - hashAlgorithm: 'sha256' | 'sha512' | 'sha1' | 'md5' = 'sha256' - ): XARFEvidence { - const payloadStr = typeof payload === 'string' ? payload : payload.toString('utf8'); - const payloadBuffer = typeof payload === 'string' ? Buffer.from(payload, 'utf8') : payload; - - const hashValue = this.generateHash(payloadBuffer, hashAlgorithm); - const hash = `${hashAlgorithm}:${hashValue}`; // Format: algorithm:hexvalue - - return { - content_type: contentType, - description, - payload: payloadStr, - hash, - }; + if (showMissingOptional && result.info) { + createReportResult.info = result.info; } - /** - * Generate a complete XARF v4.0.0 report - * @param options - Report generation options - * @returns Complete XARF report object - * @throws {XARFError} If validation fails or required fields are missing - */ - generateReport(options: GeneratorOptions): XARFReport { - const { - category, - reporter, - sender, - description, - evidence, - confidence, - tags, - additionalFields, - } = options; - - const reportType = options.type; - const sourceIdentifier = options.source_identifier; - const evidenceSource = options.evidence_source; - - // Validate all required fields - this.validateRequiredOptions(sourceIdentifier, reportType, reporter, sender); - - // After validation, these are guaranteed to be defined - const validatedReportType = reportType!; - const validatedSourceIdentifier = sourceIdentifier!; - const validatedReporter = reporter!; - const validatedSender = sender!; - - // Validate category and type - this.validateCategoryAndType(category, validatedReportType, evidenceSource); - - // Validate optional fields - this.validateConfidence(confidence); - - // Extract category-specific fields from options using schema registry - const categoryFields = this.extractCategoryFields(options, validatedReportType); - - // Merge category fields with additionalFields (additionalFields takes precedence for overrides) - const mergedFields = { ...categoryFields, ...additionalFields }; - - // Build the report - const report = this.buildCompleteReport( - { - category, - reportType: validatedReportType, - sourceIdentifier: validatedSourceIdentifier, - evidenceSource, - reporter: validatedReporter, - sender: validatedSender, - }, - { - description, - evidence, - confidence, - tags, - additionalFields: mergedFields, - } - ); - - // Validate against schema to ensure the report is valid - // This catches any missing required fields defined in type-specific schemas - // and warns about unknown fields that were passed in - const xarfValidator = new XARFValidator(); - const validationResult = xarfValidator.validate(report, false); - if (!validationResult.valid || validationResult.warnings.length > 0) { - const allIssues = [...validationResult.errors, ...validationResult.warnings]; - const messages = allIssues.map((e) => `${e.field}: ${e.message}`); - throw new XARFError(`Generated report is invalid: ${messages.join('; ')}`); - } - - return report; - } - - /** - * Base fields that are handled explicitly by the generator. - * Any fields not in this set will be passed through to the report, - * where XARFValidator will catch unknown fields. - */ - private static readonly BASE_FIELDS = new Set([ - 'category', - 'reporter', - 'sender', - 'description', - 'evidence', - 'confidence', - 'tags', - 'additionalFields', - 'type', - 'source_identifier', - 'evidence_source', - ]); - - /** - * Extract category-specific fields from generator options. - * Passes through all fields that aren't base generator options, - * allowing XARFValidator to detect unknown fields. - * @param options - Generator options containing category-specific fields - * @param _reportType - The validated report type (unused, kept for API compatibility) - * @returns Object containing category-specific and any unknown fields - */ - private extractCategoryFields( - options: GeneratorOptions, - _reportType: string - ): Record { - const fields: Record = {}; - - // Extract all fields that aren't base fields - // This allows unknown fields to pass through to the report - // where XARFValidator will catch them - const optionsRecord = options as unknown as Record; - for (const fieldName of Object.keys(optionsRecord)) { - if (!XARFGenerator.BASE_FIELDS.has(fieldName) && optionsRecord[fieldName] !== undefined) { - fields[fieldName] = optionsRecord[fieldName]; - } - } - - // Also extract source_port if provided (it's in core schema but commonly needed) - if (optionsRecord['source_port'] !== undefined) { - fields['source_port'] = optionsRecord['source_port']; - } - - return fields; - } - - /** - * Validate required options for report generation - * @param sourceIdentifier - Source identifier value - * @param reportType - Report type value - * @param reporter - Reporter contact info - * @param sender - Sender contact info - * @throws {XARFError} If required fields are missing or invalid - */ - private validateRequiredOptions( - sourceIdentifier: string | undefined, - reportType: string | undefined, - reporter: { org: string; contact: string; domain: string } | undefined, - sender: { org: string; contact: string; domain: string } | undefined - ): void { - if (!sourceIdentifier) { - throw new XARFError('source_identifier is required'); - } - if (!reportType) { - throw new XARFError('type is required'); - } - if (!reporter) { - throw new XARFError('reporter is required'); - } - if (!sender) { - throw new XARFError('sender is required'); - } - - this.validateContactInfo(reporter, 'reporter'); - this.validateContactInfo(sender, 'sender'); - } - - /** - * Validate category, type, and evidence source - * @param category - XARF category - * @param reportType - Report type - * @param evidenceSource - Evidence source - * @throws {XARFError} If category, type, or evidence source is invalid - */ - private validateCategoryAndType( - category: XARFCategory, - reportType: string, - evidenceSource?: EvidenceSource - ): void { - if (!XARFGenerator.VALID_CATEGORIES.has(category)) { - throw new XARFError( - `Invalid category '${category}'. Must be one of: ${Array.from(XARFGenerator.VALID_CATEGORIES).join(', ')}` - ); - } - - const validTypes = XARFGenerator.EVENT_TYPES[category] || []; - if (!validTypes.includes(reportType)) { - throw new XARFError( - `Invalid type '${reportType}' for category '${category}'. Must be one of: ${validTypes.join(', ')}` - ); - } - - if (evidenceSource !== undefined && !XARFGenerator.VALID_EVIDENCE_SOURCES.has(evidenceSource)) { - throw new XARFError( - `Invalid evidence_source '${evidenceSource}'. Must be one of: ${Array.from(XARFGenerator.VALID_EVIDENCE_SOURCES).join(', ')}` - ); - } - } - - /** - * Validate confidence score - * @param confidence - Optional confidence score - * @throws {XARFError} If confidence is out of range - */ - private validateConfidence(confidence: number | undefined): void { - if (confidence !== undefined && (confidence < 0.0 || confidence > 1.0)) { - throw new XARFError('confidence must be between 0.0 and 1.0'); - } - } - - /** - * Build complete XARF report with all fields - * @param required - Required report fields (typed object with nested properties) - * @param required.category - Report category (messaging, connection, content, etc.) - * @param required.reportType - Specific report type within the category - * @param required.sourceIdentifier - Source IP address or identifier - * @param required.evidenceSource - How the abuse was detected - * @param required.reporter - Reporter contact information - * @param required.reporter.org - Reporter organization name - * @param required.reporter.contact - Reporter contact email address - * @param required.reporter.domain - Reporter organization domain - * @param required.sender - Sender/ISP contact information - * @param required.sender.org - Sender organization name - * @param required.sender.contact - Sender contact email address - * @param required.sender.domain - Sender organization domain - * @param optional - Optional report fields (typed object with nested properties) - * @param optional.description - Human-readable description of the abuse - * @param optional.evidence - Array of evidence objects with payloads - * @param optional.confidence - Confidence score (0.0 to 1.0) - * @param optional.tags - Additional classification tags - * @param optional.additionalFields - Additional category-specific fields - * @returns Complete XARF report - */ - private buildCompleteReport( - required: { - category: XARFCategory; - reportType: string; - sourceIdentifier: string; - evidenceSource?: EvidenceSource; - reporter: { org: string; contact: string; domain: string }; - sender: { org: string; contact: string; domain: string }; - }, - optional: { - description?: string; - evidence?: XARFEvidence[]; - confidence?: number; - tags?: string[]; - additionalFields?: Record; - } - ): XARFReport { - const report: XARFReport = { - xarf_version: XARFGenerator.XARF_VERSION, - report_id: this.generateUUID(), - timestamp: this.generateTimestamp(), - reporter: { - org: required.reporter.org, - contact: required.reporter.contact, - domain: required.reporter.domain, - }, - sender: { - org: required.sender.org, - contact: required.sender.contact, - domain: required.sender.domain, - }, - source_identifier: required.sourceIdentifier, - category: required.category, - type: required.reportType, - }; - - if (required.evidenceSource) { - report.evidence_source = required.evidenceSource; - } - - if (optional.description) report.description = optional.description; - if (optional.evidence) report.evidence = optional.evidence; - if (optional.confidence !== undefined) report.confidence = optional.confidence; - if (optional.tags) report.tags = optional.tags; - if (optional.additionalFields) Object.assign(report, optional.additionalFields); - - return report; - } - - /** - * Validate ContactInfo structure - * @param contactInfo - Contact information to validate - * @param contactInfo.org - Organization name - * @param contactInfo.contact - Contact email address - * @param contactInfo.domain - Organization domain - * @param fieldName - Name of the field for error messages - * @throws {XARFError} If validation fails - */ - private validateContactInfo( - contactInfo: { org: string; contact: string; domain: string }, - fieldName: string - ): void { - const result = validateContactInfoUtil(contactInfo as Record, fieldName); - if (!result.valid) { - throw new XARFError(result.errors[0]); - } - } - - /** - * Generate random sample evidence for testing purposes - * @param category - Report category to determine appropriate content type - * @param description - Custom description (auto-generated if not provided) - * @returns Sample evidence item - */ - generateRandomEvidence(category: XARFCategory, description?: string): XARFEvidence { - // Select appropriate content type for category - const contentTypes = XARFGenerator.EVIDENCE_CONTENT_TYPES[category] || ['text/plain']; - const contentType = contentTypes[Math.floor(Math.random() * contentTypes.length)]; - - // Generate random payload data - const randomData = randomBytes(32); - const payload = randomData.toString('hex'); - - // Generate description if not provided - const finalDescription = description || `Sample ${category} evidence data`; - - return this.addEvidence(contentType, finalDescription, payload); - } - - /** - * Generate random sample contact data - * @returns Sample reporter and sender contact info - */ - private generateSampleContacts(): { - reporter: { org: string; contact: string; domain: string }; - sender: { org: string; contact: string; domain: string }; - } { - const sampleOrgs = [ - 'Security Operations Center', - 'Abuse Response Team', - 'Network Security Team', - 'Threat Intelligence Unit', - 'SOC Team', - ]; - const sampleDomains = ['example.com', 'security.net', 'abuse.org', 'soc.io']; - - const reporterOrg = sampleOrgs[Math.floor(Math.random() * sampleOrgs.length)]; - const senderOrg = sampleOrgs[Math.floor(Math.random() * sampleOrgs.length)]; - const reporterDomain = sampleDomains[Math.floor(Math.random() * sampleDomains.length)]; - const senderDomain = sampleDomains[Math.floor(Math.random() * sampleDomains.length)]; - - return { - reporter: { - org: reporterOrg, - contact: `abuse@${reporterDomain}`, - domain: reporterDomain, - }, - sender: { - org: senderOrg, - contact: `report@${senderDomain}`, - domain: senderDomain, - }, - }; - } - - /** - * Add optional fields to sample report - * @param options - Generator options to modify - * @param category - Report category - * @param reportType - Report type - */ - private addSampleOptionalFields( - options: GeneratorOptions, - category: XARFCategory, - reportType: string - ): void { - options.confidence = Math.round((0.7 + Math.random() * 0.3) * 100) / 100; - options.tags = [`category:${category}`, `type:${reportType}`, 'source:sample']; - } - - /** - * Generate category-specific fields for sample report - * @param category - Report category - * @param reportType - Report type - * @param includeOptional - Whether to include optional fields - * @returns Additional fields object - */ - private generateCategorySpecificFields( - category: XARFCategory, - reportType: string, - includeOptional: boolean - ): Record { - const fields: Record = {}; - - switch (category) { - case 'connection': - fields.destination_ip = `203.0.113.${Math.floor(Math.random() * 256)}`; - fields.protocol = ['tcp', 'udp', 'icmp'][Math.floor(Math.random() * 3)]; - // first_seen is required for connection types - fields.first_seen = new Date( - Date.now() - Math.floor(Math.random() * 3600000) - ).toISOString(); - // source_port is required when source_identifier is an IP - fields.source_port = 1024 + Math.floor(Math.random() * 64000); - if (includeOptional) { - fields.destination_port = [80, 443, 22, 25, 53][Math.floor(Math.random() * 5)]; - } - break; - - case 'content': - fields.url = `http://malicious${Math.floor(Math.random() * 1000)}.example.com`; - if (includeOptional) { - fields.content_type = 'text/html'; - } - break; - - case 'messaging': - // Use valid protocol values from messaging schema - fields.protocol = ['smtp', 'sms', 'chat'][Math.floor(Math.random() * 3)]; - if (fields.protocol === 'smtp') { - fields.smtp_from = `spammer${Math.floor(Math.random() * 100)}@evil.example.com`; - // source_port is required when protocol is smtp - fields.source_port = 25 + Math.floor(Math.random() * 100); - if (reportType === 'spam' || reportType === 'phishing') { - fields.subject = `Sample ${reportType} subject`; - } - if (includeOptional) { - fields.smtp_to = `victim@example.com`; - } - } - break; - - case 'infrastructure': - // compromise_evidence is required for botnet type - if (reportType === 'botnet') { - fields.compromise_evidence = 'C2 communication observed'; - } - // compromise_method is required for compromised_server type - if (reportType === 'compromised_server') { - fields.compromise_method = 'unauthorized_access'; - } - break; - - case 'copyright': - // infringing_url is required for most copyright types - fields.infringing_url = `http://pirate${Math.floor(Math.random() * 1000)}.example.com/content`; - break; - - case 'vulnerability': - // service is required for all vulnerability types - fields.service = 'http'; - if (reportType === 'cve') { - fields.service_port = 80; - fields.cve_id = 'CVE-2024-12345'; - } - break; - - case 'reputation': - // threat_type is required for reputation types - fields.threat_type = 'spam_source'; - break; - } - - return fields; - } - - /** - * Generate a sample XARF report with randomized data for testing - * @param category - Report category - * @param reportType - Specific type within category - * @param includeEvidence - Whether to include sample evidence (default: true) - * @param includeOptional - Whether to include optional fields (default: true) - * @returns Complete sample XARF report - * @throws {XARFError} If category or type is invalid - */ - generateSampleReport( - category: XARFCategory, - reportType: string, - includeEvidence = true, - includeOptional = true - ): XARFReport { - if (!XARFGenerator.VALID_CATEGORIES.has(category)) { - throw new XARFError(`Invalid category: ${category}`); - } - - const validTypes = XARFGenerator.EVENT_TYPES[category] || []; - if (!validTypes.includes(reportType)) { - throw new XARFError(`Invalid type '${reportType}' for category '${category}'`); - } - - const sourceIp = `192.0.2.${Math.floor(Math.random() * 256)}`; - const contacts = this.generateSampleContacts(); - - const options: GeneratorOptions = { - category, - type: reportType, - source_identifier: sourceIp, - reporter: contacts.reporter, - sender: contacts.sender, - description: `Sample ${reportType} report for testing`, - }; - - if (includeEvidence) { - options.evidence = [this.generateRandomEvidence(category)]; - } - - if (includeOptional) { - this.addSampleOptionalFields(options, category, reportType); - } - - options.additionalFields = this.generateCategorySpecificFields( - category, - reportType, - includeOptional - ); + return createReportResult; +} - return this.generateReport(options); +/** + * Create an evidence object with automatic base64 encoding, hashing, and size calculation. + * @param contentType - MIME type of the evidence + * @param payload - The evidence data + * @param options - Optional description and hash algorithm + * @returns Evidence object with computed hash + */ +export function createEvidence( + contentType: string, + payload: string | Buffer, + options?: EvidenceOptions +): XARFEvidence { + const hashAlgorithm = options?.hashAlgorithm ?? 'sha256'; + const payloadBuffer = typeof payload === 'string' ? Buffer.from(payload, 'utf8') : payload; + const hashValue = generateHash(payloadBuffer, hashAlgorithm); + + const evidence: XARFEvidence = { + content_type: contentType, + payload: payloadBuffer.toString('base64'), + hash: `${hashAlgorithm}:${hashValue}`, + size: payloadBuffer.length, + }; + if (options?.description !== undefined) { + evidence.description = options.description; } + return evidence; } diff --git a/src/index.ts b/src/index.ts index 563f63b..c5a7b63 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,21 +5,24 @@ * (eXtended Abuse Reporting Format) reports. */ -export { XARFParser } from './parser'; +export { SPEC_VERSION } from './version'; +export { parse, type ParseOptions, type ParseResult } from './parser'; export { - XARFGenerator, - type GeneratorOptions, - type BaseGeneratorOptions, - type ContentGeneratorOptions, - type ConnectionGeneratorOptions, - type MessagingGeneratorOptions, - type InfrastructureGeneratorOptions, - type CopyrightGeneratorOptions, - type VulnerabilityGeneratorOptions, - type ReputationGeneratorOptions, + createReport, + createEvidence, + type ReportInput, + type CreateReportOptions, + type CreateReportResult, + type EvidenceOptions, + type ConnectionReportInput, + type MessagingReportInput, + type ContentReportInput, + type InfrastructureReportInput, + type CopyrightReportInput, + type VulnerabilityReportInput, + type ReputationReportInput, } from './generator'; export { - XARFValidator, type ValidationResult, type ValidationError, type ValidationWarning, @@ -27,24 +30,90 @@ export { } from './validator'; export { SchemaValidator, validator } from './schema-validator'; export { SchemaRegistry, schemaRegistry, type FieldMetadata } from './schema-registry'; -export { XARFError, XARFValidationError, XARFParseError, XARFSchemaError } from './errors'; +export { XARFError, XARFValidationError, XARFParseError } from './errors'; export type { + // Core types XARFReport, XARFCategory, - ReporterType, EvidenceSource, - SeverityLevel, - XARFReporter, XARFEvidence, ContactInfo, + AnyXARFReport, + // Messaging + MessagingBaseReport, + SpamIndicators, + SpamReport, + BulkIndicators, + BulkMessagingReport, MessagingReport, + // Connection + ConnectionBaseReport, + LoginAttackReport, + PortScanReport, + DdosReport, + InfectedHostReport, + ReconnaissanceReport, + ScrapingReport, + SqlInjectionReport, + VulnerabilityScanReport, ConnectionReport, + // Content + ContentBaseReport, + PhishingReport, + MalwareReport, + CsamReport, + CsemReport, + ExposedDataReport, + BrandInfringementReport, + FraudReport, + CompromiseIndicator, + WebshellDetails, + RemoteCompromiseReport, + RegistrantDetails, + SuspiciousRegistrationReport, ContentReport, + // Infrastructure + InfrastructureBaseReport, + BotnetReport, + CompromisedServerReport, InfrastructureReport, + // Copyright + CopyrightBaseReport, + CopyrightCopyrightReport, + SwarmInfo, + PeerInfo, + CopyrightP2pReport, + FileInfo, + CyberlockerTakedownInfo, + CyberlockerUploaderInfo, + CopyrightCyberlockerReport, + UgcContentInfo, + UgcUploaderInfo, + UgcMatchDetails, + UgcMonetizationInfo, + CopyrightUgcPlatformReport, + LinkSiteLinkInfo, + LinkedContentItem, + LinkSiteRanking, + CopyrightLinkSiteReport, + MessageInfo, + UsenetEncodingInfo, + UsenetNzbInfo, + UsenetServerInfo, + CopyrightUsenetReport, CopyrightReport, + // Vulnerability + VulnerabilityBaseReport, + ImpactAssessment, + CveReport, + OpenServiceReport, + MisconfigurationReport, VulnerabilityReport, + // Reputation + ReputationBaseReport, + BlocklistReport, + ThreatIntelligenceReport, ReputationReport, - AnyXARFReport, } from './types'; export { @@ -57,4 +126,3 @@ export { } from './v3-legacy'; export const VERSION = '1.0.0'; -export const SPEC_VERSION = '4.0.0'; diff --git a/src/parser.ts b/src/parser.ts index 8fb0cea..6a0604a 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -2,446 +2,140 @@ * XARF v4 Parser Implementation */ -import { XARFParseError, XARFValidationError } from './errors'; -import { schemaRegistry } from './schema-registry'; -import { validator as schemaValidator } from './schema-validator'; -import { - validateContactInfo as validateContactInfoUtil, - validateTimestamp, -} from './validation-utils'; -import type { XARFReport, MessagingReport, ConnectionReport, ContentReport } from './types'; +import { XARFParseError } from './errors'; +import { XARFValidator, type ValidationInfo } from './validator'; +import type { + XARFReport, + MessagingReport, + ConnectionReport, + ContentReport, + InfrastructureReport, + CopyrightReport, + VulnerabilityReport, + ReputationReport, +} from './types'; import { isXARFv3, convertV3toV4, getV3DeprecationWarning, type XARFv3Report } from './v3-legacy'; /** - * XARF v4 Report Parser - * - * Parses and validates XARF v4 abuse reports from JSON. + * Options for parsing XARF reports */ -export class XARFParser { - private strict: boolean; - private errors: string[] = []; - private warnings: string[] = []; +export interface ParseOptions { + strict?: boolean; + showMissingOptional?: boolean; +} - /** - * Initialize parser - * @param strict - If true, raise exceptions on validation errors. - * If false, collect errors for later retrieval. - */ - constructor(strict = false) { - this.strict = strict; - } +/** + * Result of parsing a XARF report + */ +export interface ParseResult { + report: XARFReport; + errors: string[]; + warnings: string[]; + info?: ValidationInfo[]; +} - /** - * Parse JSON data into object - * @param jsonData - JSON string or object - * @returns Parsed object - * @throws {XARFParseError} If JSON parsing fails - */ - private parseJSON(jsonData: string | Record): Record { - try { - if (typeof jsonData === 'string') { - return JSON.parse(jsonData) as Record; - } - return jsonData; - } catch (error) { - throw new XARFParseError( - `Invalid JSON: ${error instanceof Error ? error.message : String(error)}` - ); - } +const validator = new XARFValidator(); + +/** + * Parse JSON data into object + * @param jsonData + * @throws {XARFParseError} If JSON parsing fails + */ +function parseJSON(jsonData: string | Record): Record { + try { + if (typeof jsonData === 'string') { + return JSON.parse(jsonData) as Record; + } + return jsonData; + } catch (error) { + throw new XARFParseError( + `Invalid JSON: ${error instanceof Error ? error.message : String(error)}` + ); } +} - /** - * Handle v3 to v4 conversion if needed - * @param data - Report data to check and possibly convert - * @returns Converted v4 data or original if already v4 - */ - private handleV3Conversion(data: Record): Record { - if (!isXARFv3(data)) { - return data; - } +/** + * Handle v3 to v4 conversion if needed + * @param data + * @param warnings + */ +function handleV3Conversion( + data: Record, + warnings: string[] +): Record { + if (!isXARFv3(data)) { + return data; + } - const conversionWarnings: string[] = []; - const v4Report = convertV3toV4(data as XARFv3Report, conversionWarnings); + const conversionWarnings: string[] = []; + const v4Report = convertV3toV4(data as XARFv3Report, conversionWarnings); - this.warnings.push(getV3DeprecationWarning()); - this.warnings.push(...conversionWarnings); + warnings.push(getV3DeprecationWarning()); + warnings.push(...conversionWarnings); - return v4Report as Record; - } + return v4Report as Record; +} - /** - * Cast data to appropriate report type based on category - * @param data - Validated report data - * @param category - Report category - * @returns Typed report object - */ - private castToReportType(data: Record, category: string): XARFReport { - if (category === 'messaging') { +/** + * Cast data to appropriate report type based on category + * @param data + * @param category + */ +function castToReportType(data: Record, category: string): XARFReport { + switch (category) { + case 'messaging': return data as MessagingReport; - } else if (category === 'connection') { + case 'connection': return data as ConnectionReport; - } else if (category === 'content') { + case 'content': return data as ContentReport; - } - return data as XARFReport; - } - - /** - * Parse XARF report from JSON - * - * Supports both XARF v4 and v3 (legacy) formats. - * v3 reports are automatically converted to v4 with a deprecation warning. - * @param jsonData - JSON string or object containing XARF report - * @returns Parsed report object - * @throws {XARFParseError} If parsing fails - * @throws {XARFValidationError} If validation fails (strict mode) - */ - parse(jsonData: string | Record): XARFReport { - this.errors = []; - this.warnings = []; - - let data = this.parseJSON(jsonData); - data = this.handleV3Conversion(data); - - if (!this.validateStructure(data)) { - if (this.strict) { - throw new XARFValidationError('Validation failed', this.errors); - } - } - - const reportCategory = data.category as string; - - if (!schemaRegistry.isValidCategory(reportCategory)) { - const validCategories = Array.from(schemaRegistry.getCategories()).join(', '); - const errorMsg = `Unsupported category '${reportCategory}'. Supported: ${validCategories}`; - if (this.strict) { - throw new XARFValidationError(errorMsg); - } - this.errors.push(errorMsg); + case 'infrastructure': + return data as InfrastructureReport; + case 'copyright': + return data as CopyrightReport; + case 'vulnerability': + return data as VulnerabilityReport; + case 'reputation': + return data as ReputationReport; + default: return data as XARFReport; - } - - try { - return this.castToReportType(data, reportCategory); - } catch (error) { - throw new XARFParseError( - `Failed to parse ${reportCategory} report: ${error instanceof Error ? error.message : String(error)}` - ); - } - } - - /** - * Validate XARF report without parsing - * - * Supports both v4 and v3 formats. v3 reports are converted before validation. - * @param jsonData - JSON string or object containing XARF report - * @returns True if valid, false otherwise - */ - validate(jsonData: string | Record): boolean { - this.errors = []; - this.warnings = []; - - let data: Record; - try { - if (typeof jsonData === 'string') { - data = JSON.parse(jsonData) as Record; - } else { - data = jsonData; - } - } catch (error) { - this.errors.push(`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`); - return false; - } - - // Check if this is a v3 report and convert it - if (isXARFv3(data)) { - const conversionWarnings: string[] = []; - const v4Report = convertV3toV4(data as XARFv3Report, conversionWarnings); - this.warnings.push(getV3DeprecationWarning()); - this.warnings.push(...conversionWarnings); - data = v4Report as Record; - } - - return this.validateStructure(data); } +} - /** - * Validate basic XARF structure - * @param data - Parsed JSON data - * @returns True if structure is valid - */ - private validateStructure(data: Record): boolean { - // Get required fields from schema registry (single source of truth) - const requiredFields = schemaRegistry.getRequiredFields(); - - // Check required fields - const dataKeys = new Set(Object.keys(data)); - const missingFields = Array.from(requiredFields).filter((field) => !dataKeys.has(field)); - if (missingFields.length > 0) { - this.errors.push(`Missing required fields: ${missingFields.join(', ')}`); - return false; - } - - // Check XARF version - if (data.xarf_version !== '4.0.0') { - this.errors.push(`Unsupported XARF version: ${data.xarf_version}`); - return false; - } - - // Validate reporter structure - if (!this.validateContactInfoStructure(data.reporter as Record, 'reporter')) { - return false; - } - - // Validate sender structure - if (!this.validateContactInfoStructure(data.sender as Record, 'sender')) { - return false; - } - - // Validate timestamp format - const timestampResult = validateTimestamp(data.timestamp as string); - if (!timestampResult.valid) { - this.errors.push(timestampResult.error!); - return false; - } - - // Check for unknown properties and emit warnings - this.checkForUnknownProperties(data); - - // Category-specific validation - if (!this.validateCategorySpecific(data)) { - return false; - } - - // Schema validation - validates against JSON schema for complete validation - const schemaResult = schemaValidator.validate(data as XARFReport); - if (!schemaResult.valid) { - this.errors.push(...schemaResult.errors); - return false; - } - - return true; - } - - /** - * Check for unknown or potentially misspelled properties - * @param data - Parsed XARF report data to check for unknown fields - */ - private checkForUnknownProperties(data: Record): void { - // Known base fields - const knownBaseFields = new Set([ - 'xarf_version', - 'report_id', - 'timestamp', - 'reporter', - 'sender', - 'source_identifier', - 'category', - 'type', - 'evidence_source', - 'on_behalf_of', - 'description', - 'evidence', - 'tags', - 'severity', - 'confidence', - 'occurrence', - 'target', - '_internal', - ]); - - // Known category-specific fields - const knownCategoryFields = new Set([ - // Messaging - 'protocol', - 'smtp_from', - 'smtp_to', - 'subject', - 'message_id', - 'sender_display_name', - 'target_victim', - 'message_content', - // Connection - 'destination_ip', - 'destination_port', - 'source_port', - 'attack_type', - 'duration_minutes', - 'packet_count', - 'byte_count', - 'attempt_count', - 'successful_logins', - 'usernames_attempted', - 'attack_pattern', - // Content - 'url', - 'content_type', - 'affected_pages', - 'cms_platform', - 'vulnerability_exploited', - 'affected_parameters', - 'payload_detected', - 'data_exposed', - 'database_type', - 'records_potentially_affected', - // Infrastructure - 'infrastructure_type', - 'affected_services', - // Copyright - 'copyright_holder', - 'infringing_content', - 'original_content', - // Vulnerability - 'cve_id', - 'vulnerability_type', - 'affected_software', - 'affected_version', - // Reputation - 'reputation_score', - 'blocklists', - ]); - - const allKnownFields = new Set([...knownBaseFields, ...knownCategoryFields]); - const dataKeys = Object.keys(data); - - for (const key of dataKeys) { - if (!allKnownFields.has(key)) { - this.warnings.push(`Unknown property '${key}' - may be ignored or misspelled`); - } - } - } - - /** - * Validate ContactInfo structure (reporter or sender) - * Uses shared validation utility for consistent validation across the codebase. - * @param contactInfo - Contact information object to validate - * @param fieldName - Name of the field being validated (for error messages) - * @returns True if contact info is valid, false otherwise - */ - private validateContactInfoStructure( - contactInfo: Record, - fieldName: string - ): boolean { - const result = validateContactInfoUtil(contactInfo, fieldName); - if (!result.valid) { - this.errors.push(...result.errors); - return false; - } - return true; - } - - /** - * Validate category-specific requirements - * @param data - Parsed JSON data - * @returns True if category-specific validation passes - */ - private validateCategorySpecific(data: Record): boolean { - const reportCategory = data.category as string; - const reportType = data.type as string; - - if (reportCategory === 'messaging') { - return this.validateMessaging(data, reportType); - } else if (reportCategory === 'connection') { - return this.validateConnection(data, reportType); - } else if (reportCategory === 'content') { - return this.validateContent(data, reportType); - } - - return true; - } - - /** - * Validate messaging category reports - * @param data - Parsed XARF report data - * @param reportType - Type of messaging report (spam, phishing, etc.) - * @returns True if validation passes, false otherwise - */ - private validateMessaging(data: Record, reportType: string): boolean { - if (!schemaRegistry.isValidType('messaging', reportType)) { - const validTypes = Array.from(schemaRegistry.getTypesForCategory('messaging')).join(', '); - this.errors.push(`Invalid messaging type: ${reportType}. Valid types: ${validTypes}`); - return false; - } - - // Email-specific validation - if (data.protocol === 'smtp') { - if (!data.smtp_from) { - this.errors.push('smtp_from required for email reports'); - return false; - } - if ((reportType === 'spam' || reportType === 'phishing') && !data.subject) { - this.errors.push('subject required for spam/phishing reports'); - return false; - } - } - - return true; - } - - /** - * Validate connection category reports - * @param data - Parsed XARF report data - * @param reportType - Type of connection report (ddos, port_scan, etc.) - * @returns True if validation passes, false otherwise - */ - private validateConnection(data: Record, reportType: string): boolean { - if (!schemaRegistry.isValidType('connection', reportType)) { - const validTypes = Array.from(schemaRegistry.getTypesForCategory('connection')).join(', '); - this.errors.push(`Invalid connection type: ${reportType}. Valid types: ${validTypes}`); - return false; - } - - // Required fields for connection reports - if (!data.destination_ip) { - this.errors.push('destination_ip required for connection reports'); - return false; - } - - if (!data.protocol) { - this.errors.push('protocol required for connection reports'); - return false; - } +/** + * Parse XARF report from JSON + * + * Supports both XARF v4 and v3 (legacy) formats. + * v3 reports are automatically converted to v4 with a deprecation warning. + * @param jsonData - JSON string or object containing XARF report + * @param options - Parse options + * @returns Parse result with report, errors, and warnings + * @throws {XARFParseError} If JSON parsing fails (malformed JSON) + */ +export function parse( + jsonData: string | Record, + options?: ParseOptions +): ParseResult { + const strict = options?.strict ?? false; + const showMissingOptional = options?.showMissingOptional ?? false; + const errors: string[] = []; + const warnings: string[] = []; - return true; - } + let data = parseJSON(jsonData); + data = handleV3Conversion(data, warnings); - /** - * Validate content category reports - * @param data - Parsed XARF report data - * @param reportType - Type of content report (phishing, malware, etc.) - * @returns True if validation passes, false otherwise - */ - private validateContent(data: Record, reportType: string): boolean { - if (!schemaRegistry.isValidType('content', reportType)) { - const validTypes = Array.from(schemaRegistry.getTypesForCategory('content')).join(', '); - this.errors.push(`Invalid content type: ${reportType}. Valid types: ${validTypes}`); - return false; - } + const result = validator.validate(data as XARFReport, strict, showMissingOptional); + errors.push(...result.errors.map((e) => `${e.field}: ${e.message}`)); + warnings.push(...result.warnings.map((w) => `${w.field}: ${w.message}`)); - // URL required for content reports - if (!data.url) { - this.errors.push('url required for content reports'); - return false; - } + const reportCategory = data.category as string; + const report = castToReportType(data, reportCategory); - return true; - } + const parseResult: ParseResult = { report, errors, warnings }; - /** - * Get validation errors from last parse/validate call - * @returns List of validation error messages - */ - getErrors(): string[] { - return [...this.errors]; + if (showMissingOptional && result.info) { + parseResult.info = result.info; } - /** - * Get warnings from last parse/validate call - * - * Warnings include deprecation notices for v3 reports and conversion issues. - * @returns List of warning messages - */ - getWarnings(): string[] { - return [...this.warnings]; - } + return parseResult; } diff --git a/src/schema-registry.ts b/src/schema-registry.ts index 718f605..c5ccc9a 100644 --- a/src/schema-registry.ts +++ b/src/schema-registry.ts @@ -7,7 +7,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import type { XARFCategory, SeverityLevel } from './types'; +import type { XARFCategory } from './types'; import { findSchemasDir, loadSchemaFile } from './schema-utils'; /** @@ -71,8 +71,6 @@ export class SchemaRegistry { // Cached validation data private categoriesCache: Set | null = null; private typesPerCategoryCache: Map> | null = null; - private evidenceSourcesCache: Set | null = null; - private severitiesCache: Set | null = null; private requiredFieldsCache: Set | null = null; private contactRequiredFieldsCache: Set | null = null; @@ -131,7 +129,8 @@ export class SchemaRegistry { const schemaPath = path.join(typesDir, file); const schema = loadSchemaFile(schemaPath); if (schema) { - this.typeSchemas.set(`${match[1]}/${match[2]}`, schema); + const normalizedType = match[2].replace(/-/g, '_'); + this.typeSchemas.set(`${match[1]}/${normalizedType}`, schema); } } } @@ -196,9 +195,7 @@ export class SchemaRegistry { if (!this.typesPerCategoryCache.has(category)) { this.typesPerCategoryCache.set(category, new Set()); } - // Convert filename format (e.g., "bulk-messaging") to schema format (e.g., "bulk_messaging") - const normalizedType = type.replace(/-/g, '_'); - this.typesPerCategoryCache.get(category)!.add(normalizedType); + this.typesPerCategoryCache.get(category)!.add(type); } } @@ -221,99 +218,6 @@ export class SchemaRegistry { return this.getTypesForCategory(category).has(type); } - /** - * Extract evidence sources from core schema examples - * @param sources - Set to add sources to - */ - private extractCoreEvidenceSources(sources: Set): void { - const examples = this.coreSchema?.properties?.evidence_source?.examples; - if (!examples) { - return; - } - for (const example of examples) { - if (typeof example === 'string') { - sources.add(example); - } - } - } - - /** - * Extract evidence sources from type schemas - * @param sources - Set to add sources to - */ - private extractTypeEvidenceSources(sources: Set): void { - for (const schema of this.typeSchemas.values()) { - this.extractEvidenceSourcesFromSchema(schema, sources); - } - } - - /** - * Extract evidence sources from a single schema - * @param schema - Schema to extract from - * @param sources - Set to add sources to - */ - private extractEvidenceSourcesFromSchema(schema: SchemaDefinition, sources: Set): void { - if (!schema.allOf) { - return; - } - for (const subSchema of schema.allOf) { - const enumValues = subSchema.properties?.evidence_source?.enum; - if (enumValues) { - enumValues.forEach((source: string) => sources.add(source)); - } - } - } - - /** - * Get valid evidence sources from schema - * @returns Set of valid evidence source values - */ - getEvidenceSources(): Set { - if (this.evidenceSourcesCache) { - return this.evidenceSourcesCache; - } - - const sources = new Set(); - this.extractCoreEvidenceSources(sources); - this.extractTypeEvidenceSources(sources); - - this.evidenceSourcesCache = sources; - return sources; - } - - /** - * Check if an evidence source is valid - * @param source - Evidence source to check - * @returns true if valid - */ - isValidEvidenceSource(source: string): boolean { - return this.getEvidenceSources().has(source); - } - - /** - * Get valid severity levels - * @returns Set of valid severity values - */ - getSeverities(): Set { - if (this.severitiesCache) { - return this.severitiesCache; - } - - // Severity is typically defined with an enum in schemas - // For now, use the standard XARF severities - this.severitiesCache = new Set(['low', 'medium', 'high', 'critical']); - return this.severitiesCache; - } - - /** - * Check if a severity is valid - * @param severity - Severity to check - * @returns true if valid - */ - isValidSeverity(severity: string): boolean { - return this.getSeverities().has(severity as SeverityLevel); - } - /** * Get required fields from core schema * @returns Set of required field names @@ -348,20 +252,8 @@ export class SchemaRegistry { * @returns Schema definition or null */ getTypeSchema(category: string, type: string): SchemaDefinition | null { - // Try exact match first - const exactKey = `${category}/${type}`; - if (this.typeSchemas.has(exactKey)) { - return this.typeSchemas.get(exactKey) || null; - } - - // Try with underscores converted to hyphens (filename format) - const hyphenatedType = type.replace(/_/g, '-'); - const hyphenKey = `${category}/${hyphenatedType}`; - if (this.typeSchemas.has(hyphenKey)) { - return this.typeSchemas.get(hyphenKey) || null; - } - - return null; + const key = `${category}/${type}`; + return this.typeSchemas.get(key) || null; } /** diff --git a/src/schema-validator.ts b/src/schema-validator.ts index 5ce6ddf..57d0e78 100644 --- a/src/schema-validator.ts +++ b/src/schema-validator.ts @@ -9,6 +9,7 @@ import type { XARFReport } from './types'; import * as fs from 'fs'; import * as path from 'path'; import { findSchemasDir } from './schema-utils'; +import { schemaRegistry } from './schema-registry'; /** * Validation result containing status and error details @@ -39,17 +40,17 @@ export interface ValidationError { */ export class SchemaValidator { private ajv: Ajv; + private strictAjv: Ajv; private coreSchemaLoaded = false; private masterSchemaLoaded = false; private schemasDir: string; /** - * Initialize SchemaValidator with AJV and format validators + * Create a configured AJV instance */ - constructor() { - // Initialize AJV with strict mode and all errors - this.ajv = new Ajv({ - strict: false, // Disable strict mode to avoid issues with $schema references + private static createAjvInstance(): Ajv { + const ajv = new Ajv({ + strict: false, // Disable strict mode to avoid issues with $schema references and x-recommended allErrors: true, verbose: true, validateFormats: true, @@ -57,15 +58,100 @@ export class SchemaValidator { // Our schemas reference JSON Schema Draft 2020-12 which AJV handles internally validateSchema: false, }); + addFormats(ajv); + return ajv; + } - // Add format validators (email, uri, date-time, etc.) - addFormats(this.ajv); + /** + * Initialize SchemaValidator with AJV and format validators + */ + constructor() { + this.ajv = SchemaValidator.createAjvInstance(); + this.strictAjv = SchemaValidator.createAjvInstance(); // Determine schemas directory path // Schemas are fetched from xarf-spec to project_root/schemas/ and copied to dist/schemas/ on build this.schemasDir = findSchemasDir(); } + /** + * Transform a schema for strict mode by promoting x-recommended properties to required. + * Deep-clones the schema and recursively walks all object definitions. + * @param schema - Original schema object + * @returns Transformed deep clone with x-recommended fields added to required arrays + */ + transformSchemaForStrict(schema: object): object { + const clone = JSON.parse(JSON.stringify(schema)); + this.promoteRecommendedToRequired(clone); + return clone; + } + + /** + * Recursively walk a schema node and add x-recommended properties to required arrays. + * Mutates the node in place. + * @param node + */ + private promoteRecommendedToRequired(node: unknown): void { + if (typeof node !== 'object' || node === null) return; + + if (Array.isArray(node)) { + for (const item of node) { + this.promoteRecommendedToRequired(item); + } + return; + } + + const obj = node as Record; + + // Promote x-recommended properties to required + if (obj.properties && typeof obj.properties === 'object' && !Array.isArray(obj.properties)) { + const properties = obj.properties as Record>; + const required = new Set( + Array.isArray(obj.required) ? (obj.required as string[]) : [] + ); + + for (const [propName, propDef] of Object.entries(properties)) { + if ( + propDef && + typeof propDef === 'object' && + !Array.isArray(propDef) && + propDef['x-recommended'] === true + ) { + required.add(propName); + } + } + + obj.required = Array.from(required); + } + + // Recurse into schema-relevant sub-structures only + const schemaKeys = [ + 'properties', + '$defs', + 'allOf', + 'anyOf', + 'oneOf', + 'items', + 'if', + 'then', + 'else', + 'not', + 'additionalProperties', + ]; + for (const key of schemaKeys) { + if (!obj[key] || typeof obj[key] !== 'object') continue; + + if (key === 'properties' || key === '$defs') { + // These are dictionaries — recurse into each value + for (const value of Object.values(obj[key] as Record)) { + this.promoteRecommendedToRequired(value); + } + } else { + this.promoteRecommendedToRequired(obj[key]); + } + } + } + /** * Load a schema file from the schemas directory * Helper method to load schemas synchronously @@ -183,6 +269,9 @@ export class SchemaValidator { try { const referencedSchema = this.loadSchemaFile(relativePath); this.ajv.addSchema(referencedSchema); + this.strictAjv.addSchema( + this.transformSchemaForStrict(referencedSchema) as Record + ); // Recursively load any schemas referenced by this schema this.loadReferencedSchemas(referencedSchema, relativePath); @@ -233,20 +322,21 @@ export class SchemaValidator { string, unknown >; + const strictCoreSchema = this.transformSchemaForStrict(coreSchema) as Record; // Register core schema under BOTH the relative path and full URL // Master schema uses relative path "xarf-core.json" const relativePath = 'xarf-core.json'; if (!this.ajv.getSchema(relativePath)) { - const schemaWithRelativeId = { ...coreSchema, $id: relativePath }; - this.ajv.addSchema(schemaWithRelativeId); + this.ajv.addSchema({ ...coreSchema, $id: relativePath }); + this.strictAjv.addSchema({ ...strictCoreSchema, $id: relativePath }); } // Also register under full URL for completeness const fullUrl = 'https://xarf.org/schemas/v4/xarf-core.json'; if (!this.ajv.getSchema(fullUrl)) { - const schemaWithFullId = { ...coreSchema, $id: fullUrl }; - this.ajv.addSchema(schemaWithFullId); + this.ajv.addSchema({ ...coreSchema, $id: fullUrl }); + this.strictAjv.addSchema({ ...strictCoreSchema, $id: fullUrl }); } this.coreSchemaLoaded = true; @@ -287,8 +377,9 @@ export class SchemaValidator { // Add schema under the FULL URL (what AJV resolves to) if (!this.ajv.getSchema(fullUrl)) { - const schemaWithFullId = { ...schema, $id: fullUrl }; - this.ajv.addSchema(schemaWithFullId); + this.ajv.addSchema({ ...schema, $id: fullUrl }); + const strictSchema = this.transformSchemaForStrict(schema) as Record; + this.strictAjv.addSchema({ ...strictSchema, $id: fullUrl }); } } catch (error) { // Ignore errors loading individual schemas @@ -321,23 +412,17 @@ export class SchemaValidator { // Load the master schema const masterSchema = this.loadSchemaFile('xarf-v4-master.json') as Record; - // Filter out missing type-specific schemas from the master schema - const filteredMasterSchema = this.filterMissingSchemas(masterSchema); - - // Add the filtered master schema + // Add the master schema to both AJV instances const masterSchemaId = 'https://xarf.org/schemas/v4/xarf-v4-master.json'; if (!this.ajv.getSchema(masterSchemaId)) { - this.ajv.addSchema(filteredMasterSchema); + this.ajv.addSchema(masterSchema); } - - // Try to compile the schema - try { - this.ajv.compile(filteredMasterSchema); - } catch (compileError) { - // If compilation still fails, silently continue - // Core schema validation will still work - // Suppress unused variable warning - void compileError; + if (!this.strictAjv.getSchema(masterSchemaId)) { + const strictMasterSchema = this.transformSchemaForStrict(masterSchema) as Record< + string, + unknown + >; + this.strictAjv.addSchema(strictMasterSchema); } this.masterSchemaLoaded = true; @@ -348,52 +433,11 @@ export class SchemaValidator { } } - /** - * Filter out references to missing type-specific schemas - * This handles cases where the master schema references schemas that don't exist - * @param schema - Master schema with anyOf array containing type-specific schema references - * @returns Filtered schema with only existing type-specific schemas referenced - */ - private filterMissingSchemas(schema: Record): Record { - // Clone the schema - const filtered = JSON.parse(JSON.stringify(schema)); - - // Find the anyOf array that contains type-specific validations - if ( - filtered.allOf && - Array.isArray(filtered.allOf) && - filtered.allOf[1] && - typeof filtered.allOf[1] === 'object' && - (filtered.allOf[1] as Record).anyOf - ) { - const anyOf = (filtered.allOf[1] as Record).anyOf as Array< - Record - >; - - // Filter out entries that reference missing schemas - const filteredAnyOf = anyOf.filter((entry: Record) => { - if (entry.then && typeof entry.then === 'object') { - const ref = (entry.then as Record).$ref; - if (typeof ref === 'string' && ref.startsWith('types/')) { - const schemaFile = ref.replace('types/', ''); - const schemaPath = path.join(this.schemasDir, 'types', schemaFile); - return fs.existsSync(schemaPath); - } - } - return true; - }); - - // Update the anyOf array - (filtered.allOf[1] as Record).anyOf = filteredAnyOf; - } - - return filtered; - } - /** * Validate a XARF report against the appropriate schema * Validates against both the core schema and the type-specific schema * @param report - The XARF report to validate + * @param strict * @returns ValidationResult with status and any error messages * @example * ```typescript @@ -404,97 +448,32 @@ export class SchemaValidator { * } * ``` */ - validate(report: XARFReport): ValidationResult { + validate(report: XARFReport, strict = false): ValidationResult { try { // Ensure schemas are loaded this.loadMasterSchema(); - const allErrors: string[] = []; - - // First validate against core schema - const coreSchema = this.ajv.getSchema('https://xarf.org/schemas/v4/xarf-core.json'); - if (coreSchema) { - const coreValid = coreSchema(report); - if (!coreValid) { - allErrors.push(...this.formatValidationErrors(coreSchema.errors || [])); - } - } - - // Then validate against type-specific schema if available - // The master schema's anyOf+if/then structure doesn't enforce type schemas properly, - // so we validate directly against the type-specific schema - const category = (report as Record).category as string | undefined; - const type = (report as Record).type as string | undefined; - - if (category && type) { - const typeSchemaId = `https://xarf.org/schemas/v4/types/${category}-${type}.json`; - const typeSchema = this.ajv.getSchema(typeSchemaId); - - if (typeSchema) { - const typeValid = typeSchema(report); - if (!typeValid) { - // Filter out duplicate errors from core schema that type schema also inherits - const typeErrors = this.formatValidationErrors(typeSchema.errors || []); - for (const err of typeErrors) { - if (!allErrors.includes(err)) { - allErrors.push(err); - } - } - } - } - } - - return { - valid: allErrors.length === 0, - errors: allErrors, - }; - } catch (error) { - // Handle unexpected validation errors - return { - valid: false, - errors: [`Validation failed: ${error instanceof Error ? error.message : String(error)}`], - }; - } - } - - /** - * Validate only against core schema (without type-specific validation) - * Useful for partial validation or testing - * @param report - The XARF report to validate - * @returns ValidationResult with status and any error messages - */ - validateCore(report: XARFReport): ValidationResult { - try { - // Ensure core schema is loaded - this.loadCoreSchema(); - - const coreSchema = this.ajv.getSchema('https://xarf.org/schemas/v4/xarf-core.json'); + const ajvInstance = strict ? this.strictAjv : this.ajv; + const masterSchemaId = 'https://xarf.org/schemas/v4/xarf-v4-master.json'; + const masterValidate = ajvInstance.getSchema(masterSchemaId); - if (!coreSchema) { - throw new Error('Core schema not found after loading'); + if (!masterValidate) { + return { valid: false, errors: ['Master schema not found after loading'] }; } - const valid = coreSchema(report); - + const valid = masterValidate(report); if (valid) { - return { - valid: true, - errors: [], - }; + return { valid: true, errors: [] }; } - const errors = this.formatValidationErrors(coreSchema.errors || []); - - return { - valid: false, - errors, - }; + // Deduplicate errors (core schema is referenced from both master and type schemas) + const errors = this.formatValidationErrors(masterValidate.errors || []); + const uniqueErrors = [...new Set(errors)]; + return { valid: false, errors: uniqueErrors }; } catch (error) { return { valid: false, - errors: [ - `Core validation failed: ${error instanceof Error ? error.message : String(error)}`, - ], + errors: [`Validation failed: ${error instanceof Error ? error.message : String(error)}`], }; } } @@ -594,9 +573,7 @@ export class SchemaValidator { * @returns true if the combination has a specific schema */ hasTypeSchema(category: string, type: string): boolean { - const schemaFile = `${category}-${type}.json`; - const schemaPath = path.join(this.schemasDir, 'types', schemaFile); - return fs.existsSync(schemaPath); + return schemaRegistry.isValidType(category, type); } /** @@ -604,27 +581,12 @@ export class SchemaValidator { * @returns Array of {category, type} objects */ getSupportedTypes(): Array<{ category: string; type: string }> { - const typesDir = path.join(this.schemasDir, 'types'); - - if (!fs.existsSync(typesDir)) { - return []; - } - - const files = fs.readdirSync(typesDir); const types: Array<{ category: string; type: string }> = []; - - for (const file of files) { - if (file.endsWith('.json') && !file.endsWith('-base.json')) { - const match = file.match(/^([^-]+)-(.+)\.json$/); - if (match) { - types.push({ - category: match[1], - type: match[2], - }); - } + for (const category of schemaRegistry.getCategories()) { + for (const type of schemaRegistry.getTypesForCategory(category)) { + types.push({ category, type }); } } - return types; } } diff --git a/src/types-connection.ts b/src/types-connection.ts new file mode 100644 index 0000000..c73ff11 --- /dev/null +++ b/src/types-connection.ts @@ -0,0 +1,138 @@ +/** + * XARF v4 Connection Category Type Definitions + */ +import type { XARFReport } from './types'; + +/** + * Connection category base report (shared fields across all connection types) + */ +export interface ConnectionBaseReport extends XARFReport { + category: 'connection'; + first_seen: string; + protocol: string; + destination_ip?: string; + destination_port?: number; + last_seen?: string; +} + +/** + * Connection - Login Attack + */ +export interface LoginAttackReport extends ConnectionBaseReport { + type: 'login_attack'; +} + +/** + * Connection - Port Scan + */ +export interface PortScanReport extends ConnectionBaseReport { + type: 'port_scan'; +} + +/** + * Connection - DDoS + */ +export interface DdosReport extends ConnectionBaseReport { + type: 'ddos'; + amplification_factor?: number; + attack_vector?: string; + duration_seconds?: number; + mitigation_applied?: boolean; + peak_bps?: number; + peak_pps?: number; + service_impact?: string; + threshold_exceeded?: string; +} + +/** + * Connection - Infected Host + */ +export interface InfectedHostReport extends ConnectionBaseReport { + type: 'infected_host'; + bot_type: string; + accepts_cookies?: boolean; + api_endpoints_accessed?: string[]; + behavior_pattern?: string; + bot_name?: string; + follows_crawl_delay?: boolean; + javascript_execution?: boolean; + request_rate?: number; + respects_robots_txt?: boolean; + total_requests?: number; + user_agent?: string; + verification_status?: string; +} + +/** + * Connection - Reconnaissance + */ +export interface ReconnaissanceReport extends ConnectionBaseReport { + type: 'reconnaissance'; + probed_resources: string[]; + automated_tool?: boolean; + http_methods?: string[]; + resource_categories?: string[]; + response_codes?: number[]; + successful_probes?: string[]; + total_probes?: number; + user_agent?: string; +} + +/** + * Connection - Scraping + */ +export interface ScrapingReport extends ConnectionBaseReport { + type: 'scraping'; + total_requests: number; + bot_signature?: string; + concurrent_connections?: number; + data_volume?: number; + request_rate?: number; + respects_robots_txt?: boolean; + scraping_pattern?: string; + session_duration?: number; + target_content?: string; + unique_urls?: number; + user_agent?: string; +} + +/** + * Connection - SQL Injection + */ +export interface SqlInjectionReport extends ConnectionBaseReport { + type: 'sql_injection'; + attack_technique?: string; + attempts_count?: number; + http_method?: string; + injection_point?: string; + payload_sample?: string; + target_url?: string; +} + +/** + * Connection - Vulnerability Scan + */ +export interface VulnerabilityScanReport extends ConnectionBaseReport { + type: 'vulnerability_scan'; + scan_type: string; + scan_rate?: number; + scanner_signature?: string; + targeted_ports?: number[]; + targeted_services?: string[]; + total_requests?: number; + user_agent?: string; + vulnerabilities_probed?: string[]; +} + +/** + * Connection category report (union of all connection types) + */ +export type ConnectionReport = + | LoginAttackReport + | PortScanReport + | DdosReport + | InfectedHostReport + | ReconnaissanceReport + | ScrapingReport + | SqlInjectionReport + | VulnerabilityScanReport; diff --git a/src/types-content.ts b/src/types-content.ts new file mode 100644 index 0000000..7a8adb3 --- /dev/null +++ b/src/types-content.ts @@ -0,0 +1,182 @@ +/** + * XARF v4 Content Category Type Definitions + */ +import type { XARFReport } from './types'; + +/** + * Content category base report (mirrors content-base.json) + */ +export interface ContentBaseReport extends XARFReport { + category: 'content'; + url: string; + domain?: string; + target_brand?: string; + verified_at?: string; + verification_method?: string; +} + +/** + * Content - Phishing + */ +export interface PhishingReport extends ContentBaseReport { + type: 'phishing'; + cloned_site?: string; + credential_fields?: string[]; + lure_type?: string; + submission_url?: string; +} + +/** + * Content - Malware + */ +export interface MalwareReport extends ContentBaseReport { + type: 'malware'; + distribution_method?: string; + file_hashes?: Record; + malware_family?: string; + malware_type?: string; +} + +/** + * Content - CSAM + */ +export interface CsamReport extends ContentBaseReport { + type: 'csam'; + classification: string; + detection_method: string; + content_removed?: boolean; + hash_values?: Record; + media_type?: string; + ncmec_report_id?: string; +} + +/** + * Content - CSEM + */ +export interface CsemReport extends ContentBaseReport { + type: 'csem'; + detection_method: string; + exploitation_type: string; + evidence_type?: string[]; + platform?: string; + reporting_obligations?: string[]; + victim_age_range?: string; +} + +/** + * Content - Exposed Data + */ +export interface ExposedDataReport extends ContentBaseReport { + type: 'exposed_data'; + data_types: string[]; + exposure_method: string; + affected_organization?: string; + encryption_status?: string; + record_count?: number; + sensitive_fields?: string[]; +} + +/** + * Content - Brand Infringement + */ +export interface BrandInfringementReport extends ContentBaseReport { + type: 'brand_infringement'; + infringement_type: string; + legitimate_site: string; + infringing_elements?: string[]; + similarity_score?: number; +} + +/** + * Content - Fraud + */ +export interface FraudReport extends ContentBaseReport { + type: 'fraud'; + fraud_type: string; + claimed_entity?: string; + payment_methods?: string[]; +} + +/** + * Compromise indicator entry + */ +export interface CompromiseIndicator { + type: + | 'file_path' + | 'process' + | 'network_connection' + | 'user_account' + | 'scheduled_task' + | 'registry_key' + | 'service'; + value: string; + description?: string; +} + +/** + * Webshell details + */ +export interface WebshellDetails { + family?: string; + capabilities?: Array< + | 'file_manager' + | 'command_execution' + | 'database_access' + | 'network_scanning' + | 'privilege_escalation' + | 'persistence' + | 'other' + >; + password_protected?: boolean; +} + +/** + * Content - Remote Compromise + */ +export interface RemoteCompromiseReport extends ContentBaseReport { + type: 'remote_compromise'; + compromise_type: string; + affected_cms?: string; + compromise_indicators?: CompromiseIndicator[]; + malicious_activities?: string[]; + persistence_mechanisms?: string[]; + webshell_details?: WebshellDetails; +} + +/** + * Suspicious domain registrant details + */ +export interface RegistrantDetails { + email_domain?: string; + country?: string; + privacy_protected?: boolean; + bulk_registrations?: number; +} + +/** + * Content - Suspicious Registration + */ +export interface SuspiciousRegistrationReport extends ContentBaseReport { + type: 'suspicious_registration'; + registration_date: string; + suspicious_indicators: string[]; + days_since_registration?: number; + predicted_usage?: string[]; + registrant_details?: RegistrantDetails; + risk_score?: number; + targeted_brands?: string[]; +} + +/** + * Content category report (union of all content types) + */ +export type ContentReport = + | PhishingReport + | MalwareReport + | CsamReport + | CsemReport + | ExposedDataReport + | BrandInfringementReport + | FraudReport + | RemoteCompromiseReport + | SuspiciousRegistrationReport; diff --git a/src/types-copyright.ts b/src/types-copyright.ts new file mode 100644 index 0000000..ac2c95d --- /dev/null +++ b/src/types-copyright.ts @@ -0,0 +1,271 @@ +/** + * XARF v4 Copyright Category Type Definitions + */ +import type { XARFReport } from './types'; + +/** + * Copyright category base report (shared fields across all copyright types) + */ +export interface CopyrightBaseReport extends XARFReport { + category: 'copyright'; + rights_holder?: string; + work_category?: string; + work_title?: string; +} + +/** + * Copyright - Copyright (direct infringement / DMCA) + */ +export interface CopyrightCopyrightReport extends CopyrightBaseReport { + type: 'copyright'; + infringing_url: string; + infringement_type?: string; + original_url?: string; +} + +/** + * P2P swarm information (info_hash or magnet_uri required at runtime via AJV) + */ +export interface SwarmInfo { + info_hash?: string; + magnet_uri?: string; + torrent_name?: string; + file_count?: number; + total_size?: number; +} + +/** + * P2P peer information + */ +export interface PeerInfo { + peer_id?: string; + client_version?: string; + upload_amount?: number; + download_amount?: number; +} + +/** + * Copyright - P2P + */ +export interface CopyrightP2pReport extends CopyrightBaseReport { + type: 'p2p'; + p2p_protocol: string; + detection_method?: string; + peer_info?: PeerInfo; + release_date?: string; + swarm_info: SwarmInfo; +} + +/** + * Cyberlocker file information + */ +export interface FileInfo { + filename?: string; + file_size?: number; + file_hash?: string; + upload_date?: string; + download_count?: number; +} + +/** + * Cyberlocker takedown information + */ +export interface CyberlockerTakedownInfo { + previous_requests?: number; + service_response_time?: string; + automated_removal?: boolean; +} + +/** + * Cyberlocker uploader information + */ +export interface CyberlockerUploaderInfo { + username?: string; + user_id?: string; + account_type?: 'free' | 'premium' | 'business' | 'unknown'; +} + +/** + * Copyright - Cyberlocker + */ +export interface CopyrightCyberlockerReport extends CopyrightBaseReport { + type: 'cyberlocker'; + hosting_service: string; + infringing_url: string; + access_method?: string; + file_info?: FileInfo; + takedown_info?: CyberlockerTakedownInfo; + uploader_info?: CyberlockerUploaderInfo; +} + +/** + * UGC platform content information + */ +export interface UgcContentInfo { + content_id?: string; + content_title?: string; + content_description?: string; + upload_date?: string; + content_duration?: number; + view_count?: number; + like_count?: number; +} + +/** + * UGC platform uploader information + */ +export interface UgcUploaderInfo { + username?: string; + user_id?: string; + account_verified?: boolean; + subscriber_count?: number; + account_creation_date?: string; +} + +/** + * UGC platform content match details + */ +export interface UgcMatchDetails { + match_confidence?: number; + match_duration?: number; + match_percentage?: number; + reference_id?: string; +} + +/** + * UGC platform monetization information + */ +export interface UgcMonetizationInfo { + monetized?: boolean; + ad_revenue?: boolean; + premium_content?: boolean; +} + +/** + * Copyright - UGC Platform + */ +export interface CopyrightUgcPlatformReport extends CopyrightBaseReport { + type: 'ugc_platform'; + infringing_url: string; + platform_name: string; + content_info?: UgcContentInfo; + infringement_type?: string; + match_details?: UgcMatchDetails; + monetization_info?: UgcMonetizationInfo; + uploader_info?: UgcUploaderInfo; +} + +/** + * Link site link information + */ +export interface LinkSiteLinkInfo { + page_title?: string; + posting_date?: string; + uploader?: string; + download_count?: number; + link_count?: number; + comments_count?: number; +} + +/** + * Link site linked content item + */ +export interface LinkedContentItem { + target_url: string; + link_type: + | 'torrent_file' + | 'magnet_link' + | 'direct_download' + | 'streaming_link' + | 'usenet_nzb' + | 'other'; + hosting_service?: string; + file_size?: number; +} + +/** + * Link site ranking information + */ +export interface LinkSiteRanking { + alexa_rank?: number; + popularity_score?: number; +} + +/** + * Copyright - Link Site + */ +export interface CopyrightLinkSiteReport extends CopyrightBaseReport { + type: 'link_site'; + infringing_url: string; + site_name: string; + link_info?: LinkSiteLinkInfo; + linked_content?: LinkedContentItem[]; + search_terms?: string[]; + site_category?: string; + site_ranking?: LinkSiteRanking; +} + +/** + * Usenet message information + */ +export interface MessageInfo { + message_id: string; + subject?: string; + from_header?: string; + posting_date?: string; + part_number?: number; + total_parts?: number; + file_size?: number; +} + +/** + * Usenet encoding information + */ +export interface UsenetEncodingInfo { + encoding_format?: 'yenc' | 'uuencode' | 'base64' | 'other'; + par2_recovery?: boolean; + rar_compression?: boolean; +} + +/** + * Usenet NZB information + */ +export interface UsenetNzbInfo { + nzb_name?: string; + nzb_url?: string; + indexer_site?: string; + completion_percentage?: number; +} + +/** + * Usenet server information + */ +export interface UsenetServerInfo { + nntp_server?: string; + server_group?: string; + retention_days?: number; +} + +/** + * Copyright - Usenet + */ +export interface CopyrightUsenetReport extends CopyrightBaseReport { + type: 'usenet'; + newsgroup: string; + detection_method?: string; + encoding_info?: UsenetEncodingInfo; + message_info: MessageInfo; + nzb_info?: UsenetNzbInfo; + server_info?: UsenetServerInfo; +} + +/** + * Copyright category report (union of all copyright types) + */ +export type CopyrightReport = + | CopyrightCopyrightReport + | CopyrightP2pReport + | CopyrightCyberlockerReport + | CopyrightUgcPlatformReport + | CopyrightLinkSiteReport + | CopyrightUsenetReport; diff --git a/src/types-infrastructure.ts b/src/types-infrastructure.ts new file mode 100644 index 0000000..b75aa70 --- /dev/null +++ b/src/types-infrastructure.ts @@ -0,0 +1,36 @@ +/** + * XARF v4 Infrastructure Category Type Definitions + */ +import type { XARFReport } from './types'; + +/** + * Infrastructure category base report + */ +export interface InfrastructureBaseReport extends XARFReport { + category: 'infrastructure'; +} + +/** + * Infrastructure - Botnet + */ +export interface BotnetReport extends InfrastructureBaseReport { + type: 'botnet'; + compromise_evidence: string; + bot_capabilities?: string[]; + c2_protocol?: string; + c2_server?: string; + malware_family?: string; +} + +/** + * Infrastructure - Compromised Server + */ +export interface CompromisedServerReport extends InfrastructureBaseReport { + type: 'compromised_server'; + compromise_method: string; +} + +/** + * Infrastructure category report (union of all infrastructure types) + */ +export type InfrastructureReport = BotnetReport | CompromisedServerReport; diff --git a/src/types-messaging.ts b/src/types-messaging.ts new file mode 100644 index 0000000..fa185cb --- /dev/null +++ b/src/types-messaging.ts @@ -0,0 +1,62 @@ +/** + * XARF v4 Messaging Category Type Definitions + */ +import type { XARFReport } from './types'; + +/** + * Messaging category base report (shared fields across all messaging types) + */ +export interface MessagingBaseReport extends XARFReport { + category: 'messaging'; + protocol: string; + sender_name?: string; + smtp_from?: string; + subject?: string; +} + +/** + * Spam analysis indicators + */ +export interface SpamIndicators { + suspicious_links?: string[]; + commercial_content?: boolean; + bulk_characteristics?: boolean; +} + +/** + * Messaging - Spam + */ +export interface SpamReport extends MessagingBaseReport { + type: 'spam'; + language?: string; + message_id?: string; + recipient_count?: number; + smtp_to?: string; + spam_indicators?: SpamIndicators; + user_agent?: string; +} + +/** + * Bulk messaging indicators + */ +export interface BulkIndicators { + high_volume?: boolean; + template_based?: boolean; + commercial_sender?: boolean; +} + +/** + * Messaging - Bulk Messaging + */ +export interface BulkMessagingReport extends MessagingBaseReport { + type: 'bulk_messaging'; + recipient_count: number; + bulk_indicators?: BulkIndicators; + opt_in_evidence?: boolean; + unsubscribe_provided?: boolean; +} + +/** + * Messaging category report (union of all messaging types) + */ +export type MessagingReport = SpamReport | BulkMessagingReport; diff --git a/src/types-reputation.ts b/src/types-reputation.ts new file mode 100644 index 0000000..39a5faa --- /dev/null +++ b/src/types-reputation.ts @@ -0,0 +1,31 @@ +/** + * XARF v4 Reputation Category Type Definitions + */ +import type { XARFReport } from './types'; + +/** + * Reputation category base report (shared fields across all reputation types) + */ +export interface ReputationBaseReport extends XARFReport { + category: 'reputation'; + threat_type: string; +} + +/** + * Reputation - Blocklist + */ +export interface BlocklistReport extends ReputationBaseReport { + type: 'blocklist'; +} + +/** + * Reputation - Threat Intelligence + */ +export interface ThreatIntelligenceReport extends ReputationBaseReport { + type: 'threat_intelligence'; +} + +/** + * Reputation category report (union of all reputation types) + */ +export type ReputationReport = BlocklistReport | ThreatIntelligenceReport; diff --git a/src/types-vulnerability.ts b/src/types-vulnerability.ts new file mode 100644 index 0000000..2623917 --- /dev/null +++ b/src/types-vulnerability.ts @@ -0,0 +1,69 @@ +/** + * XARF v4 Vulnerability Category Type Definitions + */ +import type { XARFReport } from './types'; + +/** + * Vulnerability category base report (shared fields across all vulnerability types) + */ +export interface VulnerabilityBaseReport extends XARFReport { + category: 'vulnerability'; + service: string; +} + +/** + * Impact Level for CVE impact assessment + */ +type ImpactLevel = 'none' | 'low' | 'high'; + +/** + * CVE impact assessment + */ +export interface ImpactAssessment { + confidentiality?: ImpactLevel; + integrity?: ImpactLevel; + availability?: ImpactLevel; +} + +/** + * Vulnerability - CVE (Common Vulnerabilities and Exposures) + */ +export interface CveReport extends VulnerabilityBaseReport { + type: 'cve'; + cve_id: string; + service_port: number; + cvss_score?: number; + cvss_vector?: string; + cvss_version?: string; + cve_ids?: string[]; + disclosure_date?: string; + exploitability?: string; + impact_assessment?: ImpactAssessment; + patch_available?: boolean; + patch_url?: string; + patch_version?: string; + remediation_priority?: string; + risk_level?: string; + service_version?: string; + severity?: string; + vendor_advisory?: string; +} + +/** + * Vulnerability - Open Service + */ +export interface OpenServiceReport extends VulnerabilityBaseReport { + type: 'open_service'; +} + +/** + * Vulnerability - Misconfiguration + */ +export interface MisconfigurationReport extends VulnerabilityBaseReport { + type: 'misconfiguration'; +} + +/** + * Vulnerability category report (union of all vulnerability types) + */ +export type VulnerabilityReport = CveReport | OpenServiceReport | MisconfigurationReport; diff --git a/src/types.ts b/src/types.ts index 6a04f60..48668f9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,7 +3,7 @@ */ /** - * Valid XARF categories (7 total as per XARF v4.0.0 specification) + * Valid XARF categories (7 total as per XARF v4.2.0 specification) */ export type XARFCategory = | 'messaging' @@ -15,30 +15,30 @@ export type XARFCategory = | 'reputation'; /** - * Valid reporter types - */ -export type ReporterType = 'automated' | 'manual' | 'hybrid'; - -/** - * Valid evidence sources + * Valid evidence sources. + * Known values from xarf-core.json examples are listed for autocomplete. + * Any string is accepted at the base level; type-specific schemas may + * restrict to an enum which is enforced at runtime via AJV validation. */ export type EvidenceSource = | 'spamtrap' + | 'user_complaint' + | 'automated_filter' | 'honeypot' + | 'crawler' | 'user_report' | 'automated_scan' - | 'manual_analysis' + | 'spam_analysis' + | 'firewall_logs' + | 'ids_detection' + | 'flow_analysis' | 'vulnerability_scan' | 'researcher_analysis' + | 'automated_discovery' + | 'traffic_analysis' | 'threat_intelligence' - | 'flow_analysis' - | 'ids_ips' - | 'siem'; - -/** - * Valid severity levels - */ -export type SeverityLevel = 'low' | 'medium' | 'high' | 'critical'; + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + | (string & {}); // Accepts any string while preserving autocomplete for known values /** * Contact information for reporter and sender @@ -49,24 +49,15 @@ export interface ContactInfo { domain: string; } -/** - * Reporter information (legacy type, deprecated) - * @deprecated Use ContactInfo instead - */ -export interface XARFReporter { - org?: string; - contact: string; - type: ReporterType; -} - /** * Evidence item */ export interface XARFEvidence { content_type: string; - description: string; payload: string; + description?: string; hash?: string; + size?: number; } /** @@ -85,9 +76,11 @@ export interface XARFReport { // Recommended fields (optional per XARF schema) evidence_source?: EvidenceSource; + source_port?: number; // Optional base fields description?: string; + legacy_version?: '3'; evidence?: XARFEvidence[]; tags?: string[]; confidence?: number; @@ -97,96 +90,103 @@ export interface XARFReport { [key: string]: unknown; } -/** - * Messaging category report - */ -export interface MessagingReport extends XARFReport { - category: 'messaging'; - protocol?: string; - smtp_from?: string; - smtp_to?: string; - subject?: string; - message_id?: string; - sender_display_name?: string; - target_victim?: string; - message_content?: string; -} - -/** - * Connection category report - */ -export interface ConnectionReport extends XARFReport { - category: 'connection'; - destination_ip: string; - protocol: string; - destination_port?: number; - source_port?: number; - attack_type?: string; - duration_minutes?: number; - packet_count?: number; - byte_count?: number; - attempt_count?: number; - successful_logins?: number; - usernames_attempted?: string[]; - attack_pattern?: string; -} - -/** - * Content category report - */ -export interface ContentReport extends XARFReport { - category: 'content'; - url: string; - content_type?: string; - attack_type?: string; - affected_pages?: string[]; - cms_platform?: string; - vulnerability_exploited?: string; - affected_parameters?: string[]; - payload_detected?: string; - data_exposed?: string[]; - database_type?: string; - records_potentially_affected?: number; -} - -/** - * Infrastructure category report - */ -export interface InfrastructureReport extends XARFReport { - category: 'infrastructure'; - infrastructure_type?: string; - affected_services?: string[]; -} - -/** - * Copyright category report - */ -export interface CopyrightReport extends XARFReport { - category: 'copyright'; - copyright_holder?: string; - infringing_content?: string; - original_content?: string; -} - -/** - * Vulnerability category report - */ -export interface VulnerabilityReport extends XARFReport { - category: 'vulnerability'; - cve_id?: string; - vulnerability_type?: string; - affected_software?: string; - affected_version?: string; -} - -/** - * Reputation category report - */ -export interface ReputationReport extends XARFReport { - category: 'reputation'; - reputation_score?: number; - blocklists?: string[]; -} +// Re-export category types +import { MessagingReport } from './types-messaging'; +import { ConnectionReport } from './types-connection'; +import { ContentReport } from './types-content'; +import { InfrastructureReport } from './types-infrastructure'; +import { CopyrightReport } from './types-copyright'; +import { VulnerabilityReport } from './types-vulnerability'; +import { ReputationReport } from './types-reputation'; + +export type { + MessagingBaseReport, + SpamIndicators, + SpamReport, + BulkIndicators, + BulkMessagingReport, + MessagingReport, +} from './types-messaging'; + +export type { + ConnectionBaseReport, + LoginAttackReport, + PortScanReport, + DdosReport, + InfectedHostReport, + ReconnaissanceReport, + ScrapingReport, + SqlInjectionReport, + VulnerabilityScanReport, + ConnectionReport, +} from './types-connection'; + +export type { + ContentBaseReport, + PhishingReport, + MalwareReport, + CsamReport, + CsemReport, + ExposedDataReport, + BrandInfringementReport, + FraudReport, + CompromiseIndicator, + WebshellDetails, + RemoteCompromiseReport, + RegistrantDetails, + SuspiciousRegistrationReport, + ContentReport, +} from './types-content'; + +export type { + InfrastructureBaseReport, + BotnetReport, + CompromisedServerReport, + InfrastructureReport, +} from './types-infrastructure'; + +export type { + CopyrightBaseReport, + CopyrightCopyrightReport, + SwarmInfo, + PeerInfo, + CopyrightP2pReport, + FileInfo, + CyberlockerTakedownInfo, + CyberlockerUploaderInfo, + CopyrightCyberlockerReport, + UgcContentInfo, + UgcUploaderInfo, + UgcMatchDetails, + UgcMonetizationInfo, + CopyrightUgcPlatformReport, + LinkSiteLinkInfo, + LinkedContentItem, + LinkSiteRanking, + CopyrightLinkSiteReport, + MessageInfo, + UsenetEncodingInfo, + UsenetNzbInfo, + UsenetServerInfo, + CopyrightUsenetReport, + CopyrightReport, +} from './types-copyright'; + +export type { + VulnerabilityBaseReport, + ImpactAssessment, + CveReport, + OpenServiceReport, + MisconfigurationReport, + VulnerabilityReport, +} from './types-vulnerability'; + +export type { + ReputationBaseReport, + BlocklistReport, + ThreatIntelligenceReport, + ReputationReport, +} from './types-reputation'; /** * Union type for all report types diff --git a/src/v3-legacy.ts b/src/v3-legacy.ts index 8c1690a..789b220 100644 --- a/src/v3-legacy.ts +++ b/src/v3-legacy.ts @@ -6,6 +6,7 @@ */ import type { XARFReport, XARFCategory, XARFEvidence, EvidenceSource } from './types'; +import { XARFParseError } from './errors'; /** * XARF v3 ReporterInfo structure @@ -26,6 +27,7 @@ export interface XARFv3Source { IP?: string; Port?: number; Type?: string; + URL?: string; } /** @@ -108,33 +110,36 @@ export function isXARFv3(data: Record): boolean { /** * Convert v3 evidence/attachment to v4 format * @param v3Attachments - Array of XARF v3 attachment objects + * @param warnings - Optional array to collect conversion warnings * @returns Array of XARF v4 evidence objects, or undefined if no attachments */ -function convertEvidence(v3Attachments?: XARFv3Attachment[]): XARFEvidence[] | undefined { +function convertEvidence( + v3Attachments?: XARFv3Attachment[], + warnings?: string[] +): XARFEvidence[] | undefined { if (!v3Attachments || v3Attachments.length === 0) { return undefined; } - return v3Attachments.map((attachment) => ({ - content_type: attachment.ContentType, - description: attachment.Description || 'Evidence from v3 report', - payload: attachment.Data, - })); -} - -/** - * Generate a UUID v4 for the converted report - * @returns UUID v4 string - */ -function generateUUID(): string { - // Simple UUID v4 generator - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - const r = (Math.random() * 16) | 0; - const v = c === 'x' ? r : (r & 0x3) | 0x8; - return v.toString(16); + return v3Attachments.map((attachment) => { + if (!attachment.Description) { + warnings?.push('Evidence attachment has no description, omitting field'); + } + const hashValue = createHash('sha256') + .update(Buffer.from(attachment.Data, 'base64')) + .digest('hex'); + return { + content_type: attachment.ContentType, + ...(attachment.Description ? { description: attachment.Description } : {}), + payload: attachment.Data, + hash: `sha256:${hashValue}`, + size: Buffer.from(attachment.Data, 'base64').length, + }; }); } +import { randomUUID, createHash } from 'crypto'; + /** * Convert XARF v3 report to v4 format * @param v3Report - XARF v3 report object @@ -147,12 +152,10 @@ export function convertV3toV4(v3Report: XARFv3Report, warnings?: string[]): XARF // Map v3 ReportType to v4 category and type const typeMapping = V3_TYPE_MAPPING[report.ReportType]; if (!typeMapping) { - // Unknown v3 types map to 'content' category with 'unclassified' type - const defaultMapping = { category: 'content' as XARFCategory, type: 'unclassified' }; - warnings?.push( - `Unknown v3 ReportType '${report.ReportType}', mapping to category='content', type='unclassified'` + throw new XARFParseError( + `Cannot convert v3 report: unknown ReportType '${report.ReportType}'. ` + + `Supported types: ${Object.keys(V3_TYPE_MAPPING).join(', ')}` ); - return convertWithMapping(v3Report, defaultMapping, warnings); } return convertWithMapping(v3Report, typeMapping, warnings); @@ -161,34 +164,57 @@ export function convertV3toV4(v3Report: XARFv3Report, warnings?: string[]): XARF /** * Extract source identifier from v3 report * @param report - V3 report data - * @param warnings - Optional warnings array * @returns Source identifier string */ -function extractSourceIdentifier(report: XARFv3Report['Report'], warnings?: string[]): string { +function extractSourceIdentifier(report: XARFv3Report['Report']): string { if (report.Source?.IP) { return report.Source.IP; } if (report.SourceIp) { return report.SourceIp; } - warnings?.push('No source IP found in v3 report, using "unknown" as source_identifier'); - return 'unknown'; + if (report.Source?.URL) { + return report.Source.URL; + } + if (report.Url) { + return report.Url; + } + throw new XARFParseError( + 'Cannot convert v3 report: no source identifier found (expected Source.IP, SourceIp, Source.URL, or Url)' + ); } /** * Extract contact info from v3 reporter info * @param reporterInfo - V3 reporter info + * @param warnings - Optional array to collect conversion warnings * @returns Contact info object */ -function extractContactInfo(reporterInfo: XARFv3ReporterInfo): { +function extractContactInfo( + reporterInfo: XARFv3ReporterInfo, + warnings?: string[] +): { org: string; contact: string; domain: string; } { const contact = reporterInfo.ReporterContactEmail || reporterInfo.ReporterOrgEmail; - const domain = contact.split('@')[1] || 'unknown.com'; - const org = reporterInfo.ReporterOrg || 'Unknown Organization'; - return { org, contact, domain }; + if (!contact) { + throw new XARFParseError( + 'Cannot convert v3 report: missing reporter email (ReporterContactEmail and ReporterOrgEmail are both absent)' + ); + } + const domain = contact.split('@')[1]; + if (!domain) { + throw new XARFParseError( + `Cannot convert v3 report: reporter email '${contact}' is not a valid email address` + ); + } + const org = reporterInfo.ReporterOrg; + if (!org) { + warnings?.push('No ReporterOrg found in v3 report, using "Unknown Organization"'); + } + return { org: org || 'Unknown Organization', contact, domain }; } /** @@ -196,22 +222,18 @@ function extractContactInfo(reporterInfo: XARFv3ReporterInfo): { * @param v4Report - V4 report to modify * @param category - Report category * @param v3Report - Original v3 report - * @param sourceIdentifier - Source identifier - * @param evidence - Converted evidence array */ function addCategorySpecificFields( v4Report: XARFReport, category: XARFCategory, - v3Report: XARFv3Report['Report'], - sourceIdentifier: string, - evidence?: XARFEvidence[] + v3Report: XARFv3Report['Report'] ): void { if (category === 'messaging') { addMessagingFields(v4Report, v3Report); } else if (category === 'connection') { addConnectionFields(v4Report, v3Report); } else if (category === 'content') { - addContentFields(v4Report, v3Report, sourceIdentifier, evidence); + addContentFields(v4Report, v3Report); } } @@ -221,12 +243,18 @@ function addCategorySpecificFields( * @param v3Report - Original v3 report */ function addMessagingFields(v4Report: XARFReport, v3Report: XARFv3Report['Report']): void { + const protocol = v3Report.Protocol || (v3Report.AdditionalInfo?.Protocol as string | undefined); + if (!protocol) { + throw new XARFParseError('Cannot convert v3 report: missing protocol for messaging type'); + } Object.assign(v4Report, { - protocol: v3Report.Protocol || v3Report.AdditionalInfo?.Protocol || 'smtp', + protocol, smtp_from: v3Report.SmtpMailFromAddress || v3Report.AdditionalInfo?.SMTPFrom, smtp_to: v3Report.SmtpRcptToAddress, subject: v3Report.SmtpMessageSubject || v3Report.AdditionalInfo?.Subject, - source_port: v3Report.Source?.Port || v3Report.SourcePort, + ...(v3Report.Source?.Port || v3Report.SourcePort + ? { source_port: v3Report.Source?.Port || v3Report.SourcePort } + : {}), }); } @@ -236,15 +264,21 @@ function addMessagingFields(v4Report: XARFReport, v3Report: XARFv3Report['Report * @param v3Report - Original v3 report */ function addConnectionFields(v4Report: XARFReport, v3Report: XARFv3Report['Report']): void { + if (!v3Report.Protocol) { + throw new XARFParseError('Cannot convert v3 report: missing protocol for connection type'); + } Object.assign(v4Report, { - destination_ip: v3Report.DestinationIp || 'unknown', - protocol: v3Report.Protocol || 'tcp', - // source_port is required when source_identifier is an IP (min value is 1) - source_port: v3Report.Source?.Port || v3Report.SourcePort || 1, - destination_port: v3Report.DestinationPort, - attempt_count: v3Report.AttackCount, + ...(v3Report.DestinationIp ? { destination_ip: v3Report.DestinationIp } : {}), + protocol: v3Report.Protocol, + ...(v3Report.Source?.Port || v3Report.SourcePort + ? { source_port: v3Report.Source?.Port || v3Report.SourcePort } + : {}), + ...(v3Report.DestinationPort != null ? { destination_port: v3Report.DestinationPort } : {}), // first_seen is required for connection types in v4 first_seen: v3Report.Date, + // there is no equivalent to v3's AttackCount that's general across Connection types, + // so we let it pass through as an additional property + ...(v3Report.AttackCount != null ? { attack_count: v3Report.AttackCount } : {}), }); } @@ -252,19 +286,16 @@ function addConnectionFields(v4Report: XARFReport, v3Report: XARFv3Report['Repor * Add content-specific fields to v4 report * @param v4Report - V4 report to modify * @param v3Report - Original v3 report - * @param sourceIdentifier - Source identifier for URL fallback - * @param evidence - Converted evidence array for content type */ -function addContentFields( - v4Report: XARFReport, - v3Report: XARFv3Report['Report'], - sourceIdentifier: string, - evidence?: XARFEvidence[] -): void { - Object.assign(v4Report, { - url: v3Report.Url || `http://${sourceIdentifier}`, - content_type: evidence?.[0]?.content_type || 'text/html', - }); +function addContentFields(v4Report: XARFReport, v3Report: XARFv3Report['Report']): void { + const url = + v3Report.Url || (v3Report.AdditionalInfo?.URL as string | undefined) || v3Report.Source?.URL; + if (!url) { + throw new XARFParseError( + `Cannot convert v3 report: missing URL for content type '${v4Report.type}'. Content reports require a URL field` + ); + } + Object.assign(v4Report, { url }); } /** @@ -284,15 +315,15 @@ function convertWithMapping( const report = v3Report.Report; const reporterInfo = v3Report.ReporterInfo; - const sourceIdentifier = extractSourceIdentifier(report, warnings); + const sourceIdentifier = extractSourceIdentifier(report); // Only set evidence_source if explicitly provided in v3 report - it's optional in v4 const evidenceSource = report.AdditionalInfo?.DetectionMethod as string | undefined; - const evidence = convertEvidence(report.Attachment || report.Samples); - const contactInfo = extractContactInfo(reporterInfo); + const evidence = convertEvidence(report.Attachment || report.Samples, warnings); + const contactInfo = extractContactInfo(reporterInfo, warnings); const v4Report: XARFReport & { _internal?: Record } = { - xarf_version: '4.0.0', - report_id: generateUUID(), + xarf_version: '4.2.0', + report_id: randomUUID(), timestamp: report.Date, reporter: contactInfo, sender: contactInfo, @@ -301,8 +332,8 @@ function convertWithMapping( type: mapping.type, description: report.AttackDescription, evidence, + legacy_version: '3', _internal: { - legacy_version: '3', original_report_type: report.ReportType, converted_at: new Date().toISOString(), }, @@ -313,7 +344,7 @@ function convertWithMapping( v4Report.evidence_source = evidenceSource as EvidenceSource; } - addCategorySpecificFields(v4Report, mapping.category, report, sourceIdentifier, evidence); + addCategorySpecificFields(v4Report, mapping.category, report); return v4Report; } diff --git a/src/validation-utils.ts b/src/validation-utils.ts deleted file mode 100644 index f1a45e2..0000000 --- a/src/validation-utils.ts +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Shared validation utilities for XARF - * - * Centralizes validation logic used across parser, validator, and generator - * to ensure consistent behavior and reduce code duplication. - */ - -/** - * Regular expression for validating email addresses - * Matches standard email format: local@domain.tld - */ -const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; - -/** - * Regular expression for validating domain/hostname - * Matches valid hostnames per RFC 1123 - */ -const DOMAIN_REGEX = - /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; - -/** - * Validation result for individual field checks - */ -export interface FieldValidationResult { - valid: boolean; - error?: string; -} - -/** - * Validation result for contact info - */ -export interface ContactValidationResult { - valid: boolean; - errors: string[]; -} - -/** - * Validate an email address - * @param email - Email address to validate - * @returns Validation result with error message if invalid - */ -export function validateEmail(email: string | undefined | null): FieldValidationResult { - if (!email || typeof email !== 'string') { - return { valid: false, error: 'Email is required' }; - } - if (!EMAIL_REGEX.test(email)) { - return { valid: false, error: `Invalid email format: ${email}` }; - } - return { valid: true }; -} - -/** - * Validate a domain/hostname - * @param domain - Domain to validate - * @returns Validation result with error message if invalid - */ -export function validateDomain(domain: string | undefined | null): FieldValidationResult { - if (!domain || typeof domain !== 'string') { - return { valid: false, error: 'Domain is required' }; - } - if (!DOMAIN_REGEX.test(domain)) { - return { valid: false, error: `Invalid domain format: ${domain}` }; - } - return { valid: true }; -} - -/** - * Validate an organization name - * @param org - Organization name to validate - * @returns Validation result with error message if invalid - */ -export function validateOrg(org: string | undefined | null): FieldValidationResult { - if (!org || typeof org !== 'string' || org.trim().length === 0) { - return { valid: false, error: 'Organization name is required and must be non-empty' }; - } - return { valid: true }; -} - -/** - * Validate a complete ContactInfo object (reporter, sender, on_behalf_of) - * @param contactInfo - Contact info object to validate - * @param fieldName - Name of the field for error messages (e.g., 'reporter', 'sender') - * @returns Validation result with all errors - */ -export function validateContactInfo( - contactInfo: Record | undefined | null, - fieldName: string -): ContactValidationResult { - const errors: string[] = []; - - if (!contactInfo || typeof contactInfo !== 'object') { - return { valid: false, errors: [`${fieldName} is required`] }; - } - - // Validate org - const orgResult = validateOrg(contactInfo.org as string); - if (!orgResult.valid) { - errors.push(`${fieldName}.org: ${orgResult.error}`); - } - - // Validate contact (email) - const emailResult = validateEmail(contactInfo.contact as string); - if (!emailResult.valid) { - errors.push(`${fieldName}.contact: ${emailResult.error}`); - } - - // Validate domain - const domainResult = validateDomain(contactInfo.domain as string); - if (!domainResult.valid) { - errors.push(`${fieldName}.domain: ${domainResult.error}`); - } - - return { valid: errors.length === 0, errors }; -} - -/** - * Validate an ISO 8601 timestamp - * @param timestamp - Timestamp string to validate - * @returns Validation result with error message if invalid - */ -export function validateTimestamp(timestamp: string | undefined | null): FieldValidationResult { - if (!timestamp || typeof timestamp !== 'string') { - return { valid: false, error: 'Timestamp is required' }; - } - - const date = new Date(timestamp); - if (isNaN(date.getTime())) { - return { valid: false, error: `Invalid timestamp format: ${timestamp}` }; - } - - return { valid: true }; -} diff --git a/src/validator.ts b/src/validator.ts index 20c5b0c..80f0a8d 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -4,11 +4,9 @@ * Provides advanced validation capabilities for XARF reports */ -import { XARFValidationError } from './errors'; import type { XARFReport } from './types'; -import { SchemaValidator } from './schema-validator'; +import { validator as schemaValidator } from './schema-validator'; import { schemaRegistry } from './schema-registry'; -import { validateEmail, validateDomain } from './validation-utils'; import { findSchemasDir, loadSchemaFile } from './schema-utils'; import * as path from 'path'; @@ -79,17 +77,15 @@ interface OptionalFieldInfo { /** * XARF Report Validator * - * Provides comprehensive validation for XARF v4.0.0 reports + * Provides comprehensive validation for XARF v4.2.0 reports */ export class XARFValidator { private errors: ValidationError[] = []; private warnings: ValidationWarning[] = []; private info: ValidationInfo[] = []; - private schemaValidator: SchemaValidator; private useSchemaValidation: boolean; private schemasDir: string; private coreSchemaCache: SchemaDefinition | null = null; - private typeSchemaCache: Map = new Map(); /** * Create a new XARF validator @@ -97,7 +93,6 @@ export class XARFValidator { */ constructor(useSchemaValidation = true) { this.useSchemaValidation = useSchemaValidation; - this.schemaValidator = new SchemaValidator(); this.schemasDir = findSchemasDir(); } @@ -122,17 +117,7 @@ export class XARFValidator { * @returns Type schema or null */ private getTypeSchema(category: string, type: string): SchemaDefinition | null { - const cacheKey = `${category}-${type}`; - if (this.typeSchemaCache.has(cacheKey)) { - return this.typeSchemaCache.get(cacheKey) || null; - } - - const schemaPath = path.join(this.schemasDir, 'types', `${category}-${type}.json`); - const schema = loadSchemaFile(schemaPath); - if (schema) { - this.typeSchemaCache.set(cacheKey, schema); - } - return schema; + return schemaRegistry.getTypeSchema(category, type); } /** @@ -156,6 +141,19 @@ export class XARFValidator { } } + /** + * Resolve a $ref to a base schema file (e.g. "./content-base.json") + * @param ref - Schema $ref string + * @returns Resolved schema or null + */ + private resolveBaseRef(ref: string): SchemaDefinition | null { + if (!ref.includes('-base.json')) { + return null; + } + const filename = ref.replace(/^\.\//, '').replace(/^\.\.\//, ''); + return loadSchemaFile(path.join(this.schemasDir, 'types', filename)); + } + /** * Extract optional fields from a schema * @param schema - Schema definition @@ -172,9 +170,13 @@ export class XARFValidator { // Handle allOf for type schemas if (schema.allOf) { for (const subSchema of schema.allOf) { - if (subSchema.properties) { - const subRequired = new Set([...required, ...(subSchema.required || [])]); - this.extractFromProperties(subSchema.properties, subRequired, optionalFields); + // Follow $ref to base schemas (e.g. content-base.json) + const resolved = subSchema.$ref ? this.resolveBaseRef(subSchema.$ref as string) : subSchema; + if (resolved) { + const subOptional = this.extractOptionalFields(resolved); + for (const [field, info] of subOptional) { + optionalFields.set(field, info); + } } } } @@ -259,33 +261,17 @@ export class XARFValidator { // 1. Run schema validation first (if enabled) if (this.useSchemaValidation) { - const schemaResult = this.validateWithSchema(report); + const schemaResult = this.validateWithSchema(report, strict); if (!schemaResult.valid) { // Schema validation errors are primary - add them first this.errors.push(...schemaResult.errors); } } - // 2. Run hand-coded validation for better error messages and additional checks - // Validate required fields - this.validateRequiredFields(report); - - // Validate field formats - this.validateFormats(report); - - // Validate field values - this.validateValues(report); - - // Validate category-specific requirements - this.validateCategorySpecific(report); - - // Check for unknown fields + // 2. Check for unknown fields (schemas use additionalProperties: true) this.collectUnknownFields(report); - // 3. Merge and deduplicate errors (schema errors take priority) - this.deduplicateErrors(); - - // 4. In strict mode, convert warnings to errors + // 3. In strict mode, convert warnings to errors if (strict && this.warnings.length > 0) { this.warnings.forEach((warning) => { this.errors.push({ @@ -297,7 +283,7 @@ export class XARFValidator { this.warnings = []; } - // 5. Collect missing optional fields if requested + // 4. Collect missing optional fields if requested if (showMissingOptional) { this.collectMissingOptionalFields(report); } @@ -313,28 +299,22 @@ export class XARFValidator { result.info = [...this.info]; } - if (strict && !result.valid) { - throw new XARFValidationError( - 'Validation failed', - result.errors.map((e) => `${e.field}: ${e.message}`) - ); - } - return result; } /** * Validate report using JSON schema * @param report - The XARF report to validate + * @param strict * @returns Validation result from schema validation */ - validateWithSchema(report: XARFReport): ValidationResult { + validateWithSchema(report: XARFReport, strict = false): ValidationResult { try { - const schemaResult = this.schemaValidator.validate(report); + const schemaResult = schemaValidator.validate(report, strict); // Convert schema validation errors to our format const errors: ValidationError[] = schemaResult.errors.map((err) => ({ - field: err.replace(/^\//, '').replace(/\//g, '.') || 'root', + field: (err.split(':')[0] || '').replace(/^\//, '').replace(/\//g, '.') || 'root', message: err.includes(':') ? err.split(':').slice(1).join(':').trim() : err, value: undefined, })); @@ -358,364 +338,4 @@ export class XARFValidator { }; } } - - /** - * Deduplicate errors - keep schema errors, remove duplicate hand-coded errors - */ - private deduplicateErrors(): void { - const seen = new Set(); - const uniqueErrors: ValidationError[] = []; - - for (const error of this.errors) { - const key = `${error.field}:${error.message}`; - if (!seen.has(key)) { - seen.add(key); - uniqueErrors.push(error); - } - } - - this.errors = uniqueErrors; - } - - /** - * Validate required fields are present - * @param report - XARF report to validate for required fields - */ - private validateRequiredFields(report: XARFReport): void { - // Get required fields from schema registry (single source of truth) - const required = schemaRegistry.getRequiredFields(); - - required.forEach((field) => { - if (!(field in report) || report[field as keyof XARFReport] === undefined) { - this.errors.push({ - field, - message: 'Required field is missing', - }); - } - }); - - // Validate reporter ContactInfo subfields - if (report.reporter) { - this.validateContactInfoFields(report.reporter, 'reporter'); - } - - // Validate sender ContactInfo subfields - if (report.sender) { - this.validateContactInfoFields(report.sender, 'sender'); - } - } - - /** - * Validate ContactInfo fields - * @param contactInfo - Contact information object to validate - * @param contactInfo.org - Organization name - * @param contactInfo.contact - Contact email address - * @param contactInfo.domain - Domain name - * @param fieldName - Name of the contact field being validated (reporter or sender) - */ - private validateContactInfoFields( - contactInfo: { org: string; contact: string; domain: string }, - fieldName: string - ): void { - if (!contactInfo.org) { - this.errors.push({ - field: `${fieldName}.org`, - message: `${fieldName} org is required`, - }); - } - if (!contactInfo.contact) { - this.errors.push({ - field: `${fieldName}.contact`, - message: `${fieldName} contact is required`, - }); - } - if (!contactInfo.domain) { - this.errors.push({ - field: `${fieldName}.domain`, - message: `${fieldName} domain is required`, - }); - } - } - - /** - * Validate contact info formats (email and domain) - * @param contactInfo - Contact info to validate - * @param fieldPrefix - Field name prefix (reporter or sender) - */ - private validateContactFormats( - contactInfo: { contact: string; domain: string } | undefined, - fieldPrefix: string - ): void { - if (!contactInfo) return; - - const capitalizedPrefix = fieldPrefix.charAt(0).toUpperCase() + fieldPrefix.slice(1); - - if (contactInfo.contact) { - const emailResult = validateEmail(contactInfo.contact); - if (!emailResult.valid) { - this.errors.push({ - field: `${fieldPrefix}.contact`, - message: `${capitalizedPrefix} contact must be a valid email address`, - value: contactInfo.contact, - }); - } - } - - if (contactInfo.domain) { - const domainResult = validateDomain(contactInfo.domain); - if (!domainResult.valid) { - this.errors.push({ - field: `${fieldPrefix}.domain`, - message: `${capitalizedPrefix} domain must be a valid hostname`, - value: contactInfo.domain, - }); - } - } - } - - /** - * Validate field formats - * @param report - XARF report to validate for correct field formats - */ - private validateFormats(report: XARFReport): void { - this.validateXarfVersion(report.xarf_version); - this.validateReportId(report.report_id); - this.validateTimestamp(report.timestamp); - this.validateContactFormats(report.reporter, 'reporter'); - this.validateContactFormats(report.sender, 'sender'); - this.validateConfidenceRange(report.confidence); - } - - /** - * Validate XARF version format - * @param version - XARF version string - */ - private validateXarfVersion(version: string | undefined): void { - if (version && !/^\d+\.\d+\.\d+$/.test(version)) { - this.errors.push({ - field: 'xarf_version', - message: 'Invalid version format (expected X.Y.Z)', - value: version, - }); - } - } - - /** - * Validate report ID UUID format - * @param reportId - Report ID string - */ - private validateReportId(reportId: string | undefined): void { - if ( - reportId && - !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(reportId) - ) { - this.warnings.push({ - field: 'report_id', - message: 'Report ID does not appear to be a valid UUID', - value: reportId, - }); - } - } - - /** - * Validate timestamp format - * @param timestamp - Timestamp string - */ - private validateTimestamp(timestamp: string | undefined): void { - if (!timestamp) return; - - try { - const date = new Date(timestamp); - if (isNaN(date.getTime())) { - this.errors.push({ - field: 'timestamp', - message: 'Invalid timestamp format', - value: timestamp, - }); - } - } catch { - this.errors.push({ - field: 'timestamp', - message: 'Invalid timestamp format', - value: timestamp, - }); - } - } - - /** - * Validate confidence score range - * @param confidence - Confidence score - */ - private validateConfidenceRange(confidence: number | undefined): void { - if (confidence !== undefined) { - if (typeof confidence !== 'number' || confidence < 0 || confidence > 1) { - this.errors.push({ - field: 'confidence', - message: 'Confidence must be a number between 0.0 and 1.0', - value: confidence, - }); - } - } - } - - /** - * Validate an enum value against schema-derived options - * @param fieldName - Name of the field being validated - * @param value - The value to validate - * @param validOptions - Set of valid options from schema - * @param fieldLabel - Human-readable label for error messages - */ - private validateEnumValue( - fieldName: string, - value: string | undefined, - validOptions: Set, - fieldLabel: string - ): void { - if (value && !validOptions.has(value)) { - this.errors.push({ - field: fieldName, - message: `Invalid ${fieldLabel} (must be one of: ${Array.from(validOptions).join(', ')})`, - value, - }); - } - } - - /** - * Validate type for category (dynamically from schema) - * @param report - XARF report to validate - */ - private validateTypeForCategory(report: XARFReport): void { - if (!report.category || !report.type) { - return; - } - const validTypes = schemaRegistry.getTypesForCategory(report.category); - if (validTypes.size > 0 && !validTypes.has(report.type)) { - this.errors.push({ - field: 'type', - message: `Invalid type for category '${report.category}' (must be one of: ${Array.from(validTypes).join(', ')})`, - value: report.type, - }); - } - } - - /** - * Validate field values - * @param report - XARF report to validate for correct field values - */ - private validateValues(report: XARFReport): void { - // Validate XARF version - if (report.xarf_version !== '4.0.0') { - this.errors.push({ - field: 'xarf_version', - message: 'Unsupported XARF version (expected 4.0.0)', - value: report.xarf_version, - }); - } - - // Validate category (dynamically from schema) - this.validateEnumValue('category', report.category, schemaRegistry.getCategories(), 'category'); - - // Validate evidence source (dynamically from schema) - this.validateEnumValue( - 'evidence_source', - report.evidence_source, - schemaRegistry.getEvidenceSources(), - 'evidence source' - ); - - // Validate type for category (dynamically from schema) - this.validateTypeForCategory(report); - } - - /** - * Validate category-specific requirements - * @param report - XARF report to validate for category-specific rules - */ - private validateCategorySpecific(report: XARFReport): void { - switch (report.category) { - case 'messaging': - this.validateMessagingReport(report); - break; - case 'connection': - this.validateConnectionReport(report); - break; - case 'content': - this.validateContentReport(report); - break; - } - } - - /** - * Validate messaging category reports - * @param report - XARF report with messaging category to validate - */ - private validateMessagingReport(report: XARFReport): void { - // Check for email-specific fields - if (report.protocol === 'smtp') { - if (!report.smtp_from) { - this.errors.push({ - field: 'smtp_from', - message: 'smtp_from is required for SMTP messaging reports', - }); - } - } - } - - /** - * Validate connection category reports - * @param report - XARF report with connection category to validate - */ - private validateConnectionReport(report: XARFReport): void { - // Check for required connection fields - if (!report.destination_ip) { - this.errors.push({ - field: 'destination_ip', - message: 'destination_ip is required for connection reports', - }); - } - - if (!report.protocol) { - this.errors.push({ - field: 'protocol', - message: 'protocol is required for connection reports', - }); - } - - // Validate port numbers if present - if (report.destination_port !== undefined) { - const port = Number(report.destination_port); - if (!Number.isInteger(port) || port < 0 || port > 65535) { - this.errors.push({ - field: 'destination_port', - message: 'Invalid port number (must be 0-65535)', - value: report.destination_port, - }); - } - } - } - - /** - * Validate content category reports - * @param report - XARF report with content category to validate - */ - private validateContentReport(report: XARFReport): void { - // URL is required for content reports - if (!report.url) { - this.errors.push({ - field: 'url', - message: 'url is required for content reports', - }); - } else { - // Validate URL format - try { - new URL(report.url as string); - } catch { - this.errors.push({ - field: 'url', - message: 'Invalid URL format', - value: report.url, - }); - } - } - } } diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..6cea46b --- /dev/null +++ b/src/version.ts @@ -0,0 +1,2 @@ +import pkg from '../package.json'; +export const SPEC_VERSION = pkg.xarfSpec.version.replace(/^v/, ''); diff --git a/tests/errors.test.ts b/tests/errors.test.ts index 1129921..e585fee 100644 --- a/tests/errors.test.ts +++ b/tests/errors.test.ts @@ -2,7 +2,7 @@ * Tests for XARF Error Classes */ -import { XARFError, XARFValidationError, XARFParseError, XARFSchemaError } from '../src/errors'; +import { XARFError, XARFValidationError, XARFParseError } from '../src/errors'; describe('XARFError Classes', () => { describe('XARFError', () => { @@ -73,44 +73,23 @@ describe('XARFError Classes', () => { }); }); - describe('XARFSchemaError', () => { - it('should create error with message', () => { - const error = new XARFSchemaError('Schema validation failed'); - - expect(error.message).toBe('Schema validation failed'); - expect(error.name).toBe('XARFSchemaError'); - expect(error).toBeInstanceOf(XARFError); - expect(error).toBeInstanceOf(XARFSchemaError); - }); - - it('should have correct prototype chain', () => { - const error = new XARFSchemaError('Test'); - - expect(Object.getPrototypeOf(error)).toBe(XARFSchemaError.prototype); - }); - }); - describe('Error inheritance', () => { it('should all inherit from Error', () => { const xarfError = new XARFError('Test'); const validationError = new XARFValidationError('Test'); const parseError = new XARFParseError('Test'); - const schemaError = new XARFSchemaError('Test'); expect(xarfError).toBeInstanceOf(Error); expect(validationError).toBeInstanceOf(Error); expect(parseError).toBeInstanceOf(Error); - expect(schemaError).toBeInstanceOf(Error); }); it('should all inherit from XARFError', () => { const validationError = new XARFValidationError('Test'); const parseError = new XARFParseError('Test'); - const schemaError = new XARFSchemaError('Test'); expect(validationError).toBeInstanceOf(XARFError); expect(parseError).toBeInstanceOf(XARFError); - expect(schemaError).toBeInstanceOf(XARFError); }); }); }); diff --git a/tests/fixtures.ts b/tests/fixtures.ts new file mode 100644 index 0000000..0193207 --- /dev/null +++ b/tests/fixtures.ts @@ -0,0 +1,290 @@ +/** + * Valid report fixtures for all 32 XARF v4 category+type combinations. + * + * Each fixture includes only the fields required by the type's JSON schema + * (plus core required fields). Used by schema-validation tests to verify + * that every type passes validation. + */ + +import type { XARFReport } from '../src/types'; + +const base = { + xarf_version: '4.2.0', + report_id: '550e8400-e29b-41d4-a716-446655440000', + timestamp: '2024-01-15T14:30:25Z', + reporter: { + org: 'Security Corp', + contact: 'abuse@security.example', + domain: 'security.example', + }, + sender: { + org: 'Security Corp', + contact: 'abuse@security.example', + domain: 'security.example', + }, + source_identifier: '192.0.2.100', + source_port: 12345, +} as const; + +// -- connection (8 types) -------------------------------------------------- + +const connectionBase = { + ...base, + category: 'connection' as const, + protocol: 'tcp' as const, + first_seen: '2024-01-15T09:00:00Z', +}; + +const connectionDdos: XARFReport = { ...connectionBase, type: 'ddos' }; +const connectionInfectedHost: XARFReport = { + ...connectionBase, + type: 'infected_host', + bot_type: 'malicious', +}; +const connectionLoginAttack: XARFReport = { ...connectionBase, type: 'login_attack' }; +const connectionPortScan: XARFReport = { ...connectionBase, type: 'port_scan' }; +const connectionReconnaissance: XARFReport = { + ...connectionBase, + type: 'reconnaissance', + probed_resources: ['/.env', '/.git/config'], +}; +const connectionScraping: XARFReport = { + ...connectionBase, + type: 'scraping', + total_requests: 5000, +}; +const connectionSqlInjection: XARFReport = { ...connectionBase, type: 'sql_injection' }; +const connectionVulnerabilityScan: XARFReport = { + ...connectionBase, + type: 'vulnerability_scan', + scan_type: 'port_scan', + protocol: 'tcp', +}; + +// -- content (9 types) ----------------------------------------------------- + +const contentBase = { + ...base, + category: 'content' as const, + url: 'http://malicious.example.com/page', +}; + +const contentPhishing: XARFReport = { ...contentBase, type: 'phishing' }; +const contentMalware: XARFReport = { ...contentBase, type: 'malware' }; +const contentCsam: XARFReport = { + ...contentBase, + type: 'csam', + classification: 'baseline', + detection_method: 'hash_match', +}; +const contentCsem: XARFReport = { + ...contentBase, + type: 'csem', + exploitation_type: 'distribution', + detection_method: 'user_report', +}; +const contentExposedData: XARFReport = { + ...contentBase, + type: 'exposed_data', + data_types: ['credentials'], + exposure_method: 'misconfigured_server', +}; +const contentBrandInfringement: XARFReport = { + ...contentBase, + type: 'brand_infringement', + infringement_type: 'typosquatting', + legitimate_site: 'http://legitimate.example.com', +}; +const contentFraud: XARFReport = { + ...contentBase, + type: 'fraud', + fraud_type: 'investment', +}; +const contentRemoteCompromise: XARFReport = { + ...contentBase, + type: 'remote_compromise', + compromise_type: 'webshell', +}; +const contentSuspiciousRegistration: XARFReport = { + ...contentBase, + type: 'suspicious_registration', + registration_date: '2024-01-10T00:00:00Z', + suspicious_indicators: ['typosquatting'], +}; + +// -- messaging (2 types) --------------------------------------------------- + +const messagingBase = { + ...base, + category: 'messaging' as const, + protocol: 'smtp' as const, + smtp_from: 'spammer@evil.example', + source_port: 25, +}; + +const messagingSpam: XARFReport = { ...messagingBase, type: 'spam' }; +const messagingBulkMessaging: XARFReport = { + ...messagingBase, + type: 'bulk_messaging', + recipient_count: 5000, +}; + +// -- infrastructure (2 types) ---------------------------------------------- + +const infrastructureBase = { + ...base, + category: 'infrastructure' as const, +}; + +const infrastructureBotnet: XARFReport = { + ...infrastructureBase, + type: 'botnet', + compromise_evidence: 'C2 communication observed', +}; +const infrastructureCompromisedServer: XARFReport = { + ...infrastructureBase, + type: 'compromised_server', + compromise_method: 'Exploited CVE-2024-1234', +}; + +// -- copyright (6 types) --------------------------------------------------- + +const copyrightBase = { + ...base, + category: 'copyright' as const, +}; + +const copyrightCopyright: XARFReport = { + ...copyrightBase, + type: 'copyright', + infringing_url: 'http://pirate.example.com/content', +}; +const copyrightCyberlocker: XARFReport = { + ...copyrightBase, + type: 'cyberlocker', + infringing_url: 'http://cyberlocker.example.com/file/123', + hosting_service: 'MegaUpload', +}; +const copyrightLinkSite: XARFReport = { + ...copyrightBase, + type: 'link_site', + infringing_url: 'http://links.example.com/movie', + site_name: 'PirateLinks', +}; +const copyrightP2p: XARFReport = { + ...copyrightBase, + type: 'p2p', + p2p_protocol: 'bittorrent', + swarm_info: { + info_hash: 'aabbccddee11223344556677889900aabbccddee', + }, +}; +const copyrightUgcPlatform: XARFReport = { + ...copyrightBase, + type: 'ugc_platform', + infringing_url: 'http://video.example.com/watch/456', + platform_name: 'VideoShare', +}; +const copyrightUsenet: XARFReport = { + ...copyrightBase, + type: 'usenet', + newsgroup: 'alt.binaries.test', + message_info: { + message_id: '', + }, +}; + +// -- vulnerability (3 types) ----------------------------------------------- + +const vulnerabilityBase = { + ...base, + category: 'vulnerability' as const, +}; + +const vulnerabilityCve: XARFReport = { + ...vulnerabilityBase, + type: 'cve', + service: 'Apache HTTP Server', + service_port: 80, + cve_id: 'CVE-2021-44228', +}; +const vulnerabilityMisconfiguration: XARFReport = { + ...vulnerabilityBase, + type: 'misconfiguration', + service: 'nginx', +}; +const vulnerabilityOpenService: XARFReport = { + ...vulnerabilityBase, + type: 'open_service', + service: 'memcached', +}; + +// -- reputation (2 types) -------------------------------------------------- + +const reputationBase = { + ...base, + category: 'reputation' as const, +}; + +const reputationBlocklist: XARFReport = { + ...reputationBase, + type: 'blocklist', + threat_type: 'spam_source', +}; +const reputationThreatIntelligence: XARFReport = { + ...reputationBase, + type: 'threat_intelligence', + threat_type: 'malware_distribution', +}; + +// -- exports --------------------------------------------------------------- + +/** + * All 32 valid report fixtures keyed by "category/type". + */ +export const validReports: Record = { + 'connection/ddos': connectionDdos, + 'connection/infected_host': connectionInfectedHost, + 'connection/login_attack': connectionLoginAttack, + 'connection/port_scan': connectionPortScan, + 'connection/reconnaissance': connectionReconnaissance, + 'connection/scraping': connectionScraping, + 'connection/sql_injection': connectionSqlInjection, + 'connection/vulnerability_scan': connectionVulnerabilityScan, + 'content/phishing': contentPhishing, + 'content/malware': contentMalware, + 'content/csam': contentCsam, + 'content/csem': contentCsem, + 'content/exposed_data': contentExposedData, + 'content/brand_infringement': contentBrandInfringement, + 'content/fraud': contentFraud, + 'content/remote_compromise': contentRemoteCompromise, + 'content/suspicious_registration': contentSuspiciousRegistration, + 'messaging/spam': messagingSpam, + 'messaging/bulk_messaging': messagingBulkMessaging, + 'infrastructure/botnet': infrastructureBotnet, + 'infrastructure/compromised_server': infrastructureCompromisedServer, + 'copyright/copyright': copyrightCopyright, + 'copyright/cyberlocker': copyrightCyberlocker, + 'copyright/link_site': copyrightLinkSite, + 'copyright/p2p': copyrightP2p, + 'copyright/ugc_platform': copyrightUgcPlatform, + 'copyright/usenet': copyrightUsenet, + 'vulnerability/cve': vulnerabilityCve, + 'vulnerability/misconfiguration': vulnerabilityMisconfiguration, + 'vulnerability/open_service': vulnerabilityOpenService, + 'reputation/blocklist': reputationBlocklist, + 'reputation/threat_intelligence': reputationThreatIntelligence, +}; + +/** + * Helper to get a deep copy of a fixture (safe to mutate in tests). + * @param key + */ +export function getReport(key: string): XARFReport { + const report = validReports[key]; + if (!report) { + throw new Error(`Unknown fixture key: ${key}`); + } + return JSON.parse(JSON.stringify(report)) as XARFReport; +} diff --git a/tests/generator.edge-cases.test.ts b/tests/generator.edge-cases.test.ts deleted file mode 100644 index 39cb2b0..0000000 --- a/tests/generator.edge-cases.test.ts +++ /dev/null @@ -1,254 +0,0 @@ -/** - * Edge Case Tests for XARF Generator - */ - -import { XARFGenerator } from '../src/generator'; -import { XARFError } from '../src/errors'; - -describe('XARFGenerator Edge Cases', () => { - let generator: XARFGenerator; - - beforeEach(() => { - generator = new XARFGenerator(); - }); - - describe('generateReport validation edge cases', () => { - it('should throw error for missing reporter', () => { - expect(() => { - generator.generateReport({ - category: 'connection', - type: 'ddos', - source_identifier: '192.0.2.1', - reporter: null as any, - sender: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - }); - }).toThrow(XARFError); - expect(() => { - generator.generateReport({ - category: 'connection', - type: 'ddos', - source_identifier: '192.0.2.1', - reporter: null as any, - sender: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - }); - }).toThrow('reporter is required'); - }); - - it('should throw error for invalid reporter contact', () => { - expect(() => { - generator.generateReport({ - category: 'connection', - type: 'ddos', - source_identifier: '192.0.2.1', - reporter: { - org: 'Example Org', - contact: 'invalid-email', - domain: 'example.com', - }, - sender: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - }); - }).toThrow(XARFError); - expect(() => { - generator.generateReport({ - category: 'connection', - type: 'ddos', - source_identifier: '192.0.2.1', - reporter: { - org: 'Example Org', - contact: 'invalid-email', - domain: 'example.com', - }, - sender: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - }); - }).toThrow('Invalid email format'); - }); - - it('should throw error for invalid evidence_source', () => { - expect(() => { - generator.generateReport({ - category: 'connection', - type: 'ddos', - source_identifier: '192.0.2.1', - reporter: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - evidence_source: 'invalid_source' as any, - }); - }).toThrow(XARFError); - expect(() => { - generator.generateReport({ - category: 'connection', - type: 'ddos', - source_identifier: '192.0.2.1', - reporter: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - evidence_source: 'invalid_source' as any, - }); - }).toThrow('Invalid evidence_source'); - }); - - it('should throw error for confidence less than 0', () => { - expect(() => { - generator.generateReport({ - category: 'connection', - type: 'ddos', - source_identifier: '192.0.2.1', - reporter: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - confidence: -0.1, - }); - }).toThrow(XARFError); - expect(() => { - generator.generateReport({ - category: 'connection', - type: 'ddos', - source_identifier: '192.0.2.1', - reporter: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - confidence: -0.1, - }); - }).toThrow('confidence must be between 0.0 and 1.0'); - }); - }); - - describe('generateRandomEvidence edge cases', () => { - it('should handle reputation category with application/json', () => { - const evidence = generator.generateRandomEvidence('reputation'); - - expect(evidence.content_type).toBeDefined(); - expect(['text/plain', 'application/json', 'text/csv']).toContain(evidence.content_type); - expect(evidence.description).toContain('reputation'); - }); - - it('should generate evidence for all 7 categories', () => { - const categories: Array< - | 'messaging' - | 'connection' - | 'content' - | 'infrastructure' - | 'copyright' - | 'vulnerability' - | 'reputation' - > = [ - 'messaging', - 'connection', - 'content', - 'infrastructure', - 'copyright', - 'vulnerability', - 'reputation', - ]; - - categories.forEach((category) => { - const evidence = generator.generateRandomEvidence(category); - expect(evidence.content_type).toBeDefined(); - expect(evidence.payload).toBeDefined(); - expect(evidence.hash).toBeDefined(); - expect(evidence.description).toContain(category); - }); - }); - }); - - describe('generateSampleReport with various options', () => { - it('should generate sample with evidence', () => { - const report = generator.generateSampleReport('messaging', 'spam', true, false); - - expect(report.evidence).toBeDefined(); - }); - - it('should generate sample for all valid categories', () => { - const testCases: Array<{ - category: - | 'messaging' - | 'connection' - | 'content' - | 'infrastructure' - | 'copyright' - | 'vulnerability' - | 'reputation'; - type: string; - }> = [ - { category: 'messaging', type: 'spam' }, - { category: 'connection', type: 'ddos' }, - { category: 'content', type: 'phishing' }, - { category: 'infrastructure', type: 'botnet' }, - { category: 'copyright', type: 'copyright' }, - { category: 'vulnerability', type: 'cve' }, - { category: 'reputation', type: 'blocklist' }, - ]; - - testCases.forEach(({ category, type }) => { - const report = generator.generateSampleReport(category, type, true, false); - expect(report.category).toBe(category); - expect(report.type).toBe(type); - }); - }); - }); - - describe('addEvidence with different algorithms', () => { - it('should create evidence with sha512', () => { - const evidence = generator.addEvidence('text/plain', 'Test', 'data', 'sha512'); - - expect(evidence.hash).toMatch(/^sha512:[0-9a-f]{128}$/); // Format: algorithm:hexvalue - }); - - it('should create evidence with sha1', () => { - const evidence = generator.addEvidence('text/plain', 'Test', 'data', 'sha1'); - - expect(evidence.hash).toMatch(/^sha1:[0-9a-f]{40}$/); // Format: algorithm:hexvalue - }); - - it('should create evidence with md5', () => { - const evidence = generator.addEvidence('text/plain', 'Test', 'data', 'md5'); - - expect(evidence.hash).toMatch(/^md5:[0-9a-f]{32}$/); // Format: algorithm:hexvalue - }); - }); -}); diff --git a/tests/generator.test.ts b/tests/generator.test.ts index 04a009c..39f0a69 100644 --- a/tests/generator.test.ts +++ b/tests/generator.test.ts @@ -2,335 +2,343 @@ * Tests for XARF Generator */ -import { XARFGenerator } from '../src/generator'; -import { XARFError } from '../src/errors'; +import { createReport, createEvidence } from '../src/generator'; +import { SPEC_VERSION } from '../src/version'; + +const baseInput = { + reporter: { + org: 'Test Org', + contact: 'abuse@example.com', + domain: 'example.com', + }, + sender: { + org: 'Test Org', + contact: 'abuse@example.com', + domain: 'example.com', + }, + source_identifier: '192.0.2.1', +}; + +describe('createEvidence', () => { + it('should base64-encode string payload and compute sha256 hash by default', () => { + const evidence = createEvidence('text/plain', 'Sample data', { + description: 'Test evidence', + }); + + expect(evidence.content_type).toBe('text/plain'); + expect(evidence.description).toBe('Test evidence'); + expect(evidence.payload).toBe(Buffer.from('Sample data').toString('base64')); + expect(evidence.size).toBe(Buffer.from('Sample data').length); + expect(evidence.hash).toMatch(/^sha256:[0-9a-f]{64}$/); + }); -describe('XARFGenerator', () => { - let generator: XARFGenerator; + it('should base64-encode Buffer payload', () => { + const buffer = Buffer.from('test data', 'utf8'); + const evidence = createEvidence('application/octet-stream', buffer); - beforeEach(() => { - generator = new XARFGenerator(); + expect(evidence.payload).toBe(buffer.toString('base64')); + expect(evidence.size).toBe(buffer.length); + expect(evidence.hash).toMatch(/^sha256:/); }); - describe('generateUUID', () => { - it('should generate valid UUID', () => { - const uuid = generator.generateUUID(); - expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); - }); + it('should use the requested hash algorithm', () => { + const sha512 = createEvidence('text/plain', 'data', { hashAlgorithm: 'sha512' }); + expect(sha512.hash).toMatch(/^sha512:[0-9a-f]{128}$/); - it('should generate unique UUIDs', () => { - const uuid1 = generator.generateUUID(); - const uuid2 = generator.generateUUID(); - expect(uuid1).not.toBe(uuid2); - }); + const sha1 = createEvidence('text/plain', 'data', { hashAlgorithm: 'sha1' }); + expect(sha1.hash).toMatch(/^sha1:[0-9a-f]{40}$/); + + const md5 = createEvidence('text/plain', 'data', { hashAlgorithm: 'md5' }); + expect(md5.hash).toMatch(/^md5:[0-9a-f]{32}$/); }); - describe('generateTimestamp', () => { - it('should generate ISO 8601 timestamp', () => { - const timestamp = generator.generateTimestamp(); - expect(timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); - }); + it('should include description when provided', () => { + const evidence = createEvidence('text/plain', 'data', { description: 'Log excerpt' }); - it('should be parseable as date', () => { - const timestamp = generator.generateTimestamp(); - const date = new Date(timestamp); - expect(date.toISOString()).toBe(timestamp); - }); + expect(evidence.description).toBe('Log excerpt'); }); +}); + +describe('createReport', () => { + describe('auto-generated metadata', () => { + it('should set xarf_version, report_id, and timestamp automatically', () => { + const { report } = createReport({ + ...baseInput, + category: 'connection', + type: 'ddos', + protocol: 'tcp', + first_seen: '2024-01-15T09:00:00Z', + source_port: 12345, + }); - describe('generateHash', () => { - it('should generate SHA256 hash by default', () => { - const hash = generator.generateHash('test data'); - expect(hash).toHaveLength(64); - expect(hash).toMatch(/^[0-9a-f]{64}$/); + expect(report.xarf_version).toBe(SPEC_VERSION); + expect(report.report_id).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ + ); + expect(report.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/); }); - it('should generate consistent hashes', () => { - const hash1 = generator.generateHash('test data'); - const hash2 = generator.generateHash('test data'); - expect(hash1).toBe(hash2); + it('should use provided report_id and timestamp when given', () => { + const { report } = createReport({ + ...baseInput, + category: 'connection', + type: 'ddos', + protocol: 'tcp', + first_seen: '2024-01-15T09:00:00Z', + source_port: 12345, + report_id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + timestamp: '2024-06-01T00:00:00Z', + }); + + expect(report.report_id).toBe('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'); + expect(report.timestamp).toBe('2024-06-01T00:00:00Z'); }); + }); - it('should support different algorithms', () => { - const sha256 = generator.generateHash('test', 'sha256'); - const sha512 = generator.generateHash('test', 'sha512'); - const sha1 = generator.generateHash('test', 'sha1'); - const md5 = generator.generateHash('test', 'md5'); + describe('validation errors', () => { + it('should return no errors for a valid report', () => { + const { errors } = createReport({ + ...baseInput, + category: 'content', + type: 'phishing', + url: 'http://phishing.example.com', + }); - expect(sha256).toHaveLength(64); - expect(sha512).toHaveLength(128); - expect(sha1).toHaveLength(40); - expect(md5).toHaveLength(32); + expect(errors).toHaveLength(0); }); - it('should throw error for unsupported algorithm', () => { - expect(() => { - generator.generateHash('test', 'invalid' as any); - }).toThrow(XARFError); + it('should return errors for invalid category', () => { + const { errors } = createReport({ + ...baseInput, + category: 'invalid' as any, + type: 'test', + } as any); + + expect(errors.length).toBeGreaterThan(0); + expect(errors.some((e) => e.field === 'category')).toBe(true); }); - }); - describe('addEvidence', () => { - it('should create evidence with hash', () => { - const evidence = generator.addEvidence('text/plain', 'Test evidence', 'Sample data'); + it('should return errors for mismatched type and category', () => { + const { errors } = createReport({ + ...baseInput, + category: 'connection', + type: 'spam', + } as any); - expect(evidence.content_type).toBe('text/plain'); - expect(evidence.description).toBe('Test evidence'); - expect(evidence.payload).toBe('Sample data'); - expect(evidence.hash).toBeDefined(); - expect(evidence.hash).toMatch(/^sha256:[0-9a-f]{64}$/); // Format: algorithm:hexvalue + expect(errors.length).toBeGreaterThan(0); }); - it('should handle buffer payloads', () => { - const buffer = Buffer.from('test data', 'utf8'); - const evidence = generator.addEvidence('application/octet-stream', 'Binary data', buffer); + it('should return errors for missing source_identifier', () => { + const { errors } = createReport({ + reporter: baseInput.reporter, + sender: baseInput.sender, + category: 'connection', + type: 'ddos', + } as any); - expect(evidence.payload).toBe('test data'); - expect(evidence.hash).toBeDefined(); + expect(errors.length).toBeGreaterThan(0); + expect(errors.some((e) => e.message.includes('source_identifier'))).toBe(true); }); - }); - describe('generateReport', () => { - it('should generate valid connection report', () => { - const report = generator.generateReport({ + it('should return errors for null reporter', () => { + const { errors } = createReport({ category: 'connection', type: 'ddos', - source_identifier: '192.0.2.100', - reporter: { - org: 'Example Security', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Example Security', - contact: 'abuse@example.com', - domain: 'example.com', - }, - additionalFields: { - destination_ip: '203.0.113.10', - protocol: 'tcp', - first_seen: '2024-01-15T09:00:00Z', - source_port: 12345, - }, - }); + source_identifier: '192.0.2.1', + reporter: null as any, + sender: baseInput.sender, + } as any); - expect(report.xarf_version).toBe('4.0.0'); - expect(report.category).toBe('connection'); - expect(report.type).toBe('ddos'); - expect(report.source_identifier).toBe('192.0.2.100'); - expect(report.reporter.contact).toBe('abuse@example.com'); - expect(report.reporter.org).toBe('Example Security'); - expect(report.reporter.domain).toBe('example.com'); - expect(report.sender.contact).toBe('abuse@example.com'); - expect(report.sender.org).toBe('Example Security'); - expect(report.sender.domain).toBe('example.com'); - expect(report.report_id).toBeDefined(); - expect(report.timestamp).toBeDefined(); + expect(errors.length).toBeGreaterThan(0); + expect(errors.some((e) => e.field.includes('reporter'))).toBe(true); }); - it('should include optional fields', () => { - const evidence = generator.addEvidence('text/plain', 'Test', 'data'); - const report = generator.generateReport({ - category: 'content', - type: 'phishing', - source_identifier: '192.0.2.100', + it('should return errors for invalid reporter contact email', () => { + const { errors } = createReport({ + ...baseInput, + category: 'connection', + type: 'ddos', reporter: { org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Example Org', - contact: 'abuse@example.com', + contact: 'invalid-email', domain: 'example.com', }, - description: 'Test phishing site', - evidence: [evidence], - confidence: 0.95, - tags: ['type:phishing', 'source:test'], - additionalFields: { url: 'http://phishing.example.com' }, - }); + } as any); - expect(report.description).toBe('Test phishing site'); - expect(report.evidence).toHaveLength(1); - expect(report.confidence).toBe(0.95); - expect(report.tags).toContain('type:phishing'); - expect(report.url).toBe('http://phishing.example.com'); - }); - - it('should throw error for missing source identifier', () => { - expect(() => { - generator.generateReport({ - category: 'connection', - type: 'ddos', - source_identifier: '', - reporter: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - }); - }).toThrow(XARFError); + expect(errors.length).toBeGreaterThan(0); + expect(errors.some((e) => e.field.includes('contact'))).toBe(true); }); - it('should throw error for invalid category', () => { - expect(() => { - generator.generateReport({ - category: 'invalid' as any, - type: 'test', - source_identifier: '192.0.2.1', - reporter: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - }); - }).toThrow(XARFError); - }); + it('should return errors for invalid evidence_source enum value', () => { + const { errors } = createReport({ + ...baseInput, + category: 'connection', + type: 'ddos', + evidence_source: 'invalid_source' as any, + } as any); - it('should throw error for invalid type for category', () => { - expect(() => { - generator.generateReport({ - category: 'connection', - type: 'spam', - source_identifier: '192.0.2.1', - reporter: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - }); - }).toThrow(XARFError); + expect(errors.length).toBeGreaterThan(0); + expect(errors.some((e) => e.field === 'evidence_source')).toBe(true); }); - it('should throw error for invalid confidence', () => { - expect(() => { - generator.generateReport({ - category: 'connection', - type: 'ddos', - source_identifier: '192.0.2.1', - reporter: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - confidence: 1.5, - }); - }).toThrow(XARFError); - }); + it('should return errors for confidence outside 0-1 range', () => { + const { errors: tooHigh } = createReport({ + ...baseInput, + category: 'connection', + type: 'ddos', + confidence: 1.5, + } as any); + expect(tooHigh.length).toBeGreaterThan(0); - it('should throw error for unknown fields', () => { - expect(() => { - generator.generateReport({ - category: 'messaging', - type: 'spam', - source_identifier: '192.0.2.1', - reporter: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Example Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - additionalFields: { unknown_field: 'test' }, - }); - }).toThrow(XARFError); + const { errors: negative } = createReport({ + ...baseInput, + category: 'connection', + type: 'ddos', + confidence: -0.1, + } as any); + expect(negative.length).toBeGreaterThan(0); }); - }); - describe('generateRandomEvidence', () => { - it('should generate random evidence for category', () => { - const evidence = generator.generateRandomEvidence('connection'); + it('should return warnings for unknown fields in non-strict mode', () => { + const { errors, warnings } = createReport({ + ...baseInput, + category: 'content', + type: 'phishing', + url: 'http://phishing.example.com', + unknown_field: 'test', + } as any); - expect(evidence.content_type).toBeDefined(); - expect(evidence.description).toContain('connection'); - expect(evidence.payload).toBeDefined(); - expect(evidence.hash).toBeDefined(); + expect(errors).toHaveLength(0); + expect(warnings.some((w) => w.field === 'unknown_field')).toBe(true); }); - it('should use custom description', () => { - const evidence = generator.generateRandomEvidence('messaging', 'Custom description'); - - expect(evidence.description).toBe('Custom description'); + it('should promote unknown field warnings to errors in strict mode', () => { + const { errors } = createReport( + { + ...baseInput, + category: 'content', + type: 'phishing', + url: 'http://phishing.example.com', + unknown_field: 'test', + } as any, + { strict: true } + ); + + expect(errors.length).toBeGreaterThan(0); + expect(errors.some((e) => e.field === 'unknown_field')).toBe(true); }); }); - describe('generateSampleReport', () => { - it('should generate sample connection report', () => { - const report = generator.generateSampleReport('connection', 'ddos', true, false); + describe('field passthrough', () => { + it('should preserve core fields in output', () => { + const { report } = createReport({ + ...baseInput, + category: 'content', + type: 'phishing', + description: 'Test phishing site', + confidence: 0.95, + tags: ['type:phishing', 'source:test'], + url: 'http://phishing.example.com', + }); - expect(report.category).toBe('connection'); - expect(report.type).toBe('ddos'); - expect(report.source_identifier).toMatch(/^192\.0\.2\.\d+$/); - expect(report.reporter.contact).toContain('@'); - expect(report.evidence).toBeDefined(); + expect(report.source_identifier).toBe('192.0.2.1'); + expect(report.reporter.contact).toBe('abuse@example.com'); + expect(report.sender.org).toBe('Test Org'); + expect(report.description).toBe('Test phishing site'); + expect(report.confidence).toBe(0.95); + expect(report.tags).toContain('type:phishing'); }); - it('should generate sample without evidence', () => { - const report = generator.generateSampleReport('messaging', 'spam', false, false); + it('should preserve evidence array in output', () => { + const evidence = createEvidence('text/plain', 'data', { description: 'Test' }); + const { report } = createReport({ + ...baseInput, + category: 'content', + type: 'phishing', + url: 'http://phishing.example.com', + evidence: [evidence], + }); - expect(report.evidence).toBeUndefined(); + expect(report.evidence).toHaveLength(1); + expect(report.evidence![0].content_type).toBe('text/plain'); }); - it('should throw error for invalid category', () => { - expect(() => { - generator.generateSampleReport('invalid' as any, 'test'); - }).toThrow(XARFError); - }); + it('should preserve connection-specific fields', () => { + const { report, errors } = createReport({ + ...baseInput, + category: 'connection', + type: 'ddos', + destination_ip: '203.0.113.10', + protocol: 'tcp', + first_seen: '2024-01-15T09:00:00Z', + source_port: 12345, + destination_port: 80, + attack_vector: 'syn_flood', + peak_pps: 1000000, + }); - it('should throw error for invalid type', () => { - expect(() => { - generator.generateSampleReport('connection', 'invalid'); - }).toThrow(XARFError); + expect(errors).toHaveLength(0); + expect(report.destination_ip).toBe('203.0.113.10'); + expect(report.protocol).toBe('tcp'); + expect(report.destination_port).toBe(80); + expect(report.attack_vector).toBe('syn_flood'); + expect(report.peak_pps).toBe(1000000); }); - }); - describe('static constants', () => { - it('should have correct XARF version', () => { - expect(XARFGenerator.XARF_VERSION).toBe('4.0.0'); - }); + it('should preserve messaging-specific fields', () => { + const { report, errors } = createReport({ + ...baseInput, + category: 'messaging', + type: 'spam', + protocol: 'smtp', + smtp_from: 'spammer@evil.example.com', + source_port: 25, + smtp_to: 'victim@example.com', + subject: 'You won!', + message_id: '<123456@evil.example.com>', + }); - it('should have all 7 valid categories per XARF v4.0.0 spec', () => { - expect(XARFGenerator.VALID_CATEGORIES.size).toBe(7); - expect(XARFGenerator.VALID_CATEGORIES.has('messaging')).toBe(true); - expect(XARFGenerator.VALID_CATEGORIES.has('connection')).toBe(true); - expect(XARFGenerator.VALID_CATEGORIES.has('content')).toBe(true); - expect(XARFGenerator.VALID_CATEGORIES.has('infrastructure')).toBe(true); - expect(XARFGenerator.VALID_CATEGORIES.has('copyright')).toBe(true); - expect(XARFGenerator.VALID_CATEGORIES.has('vulnerability')).toBe(true); - expect(XARFGenerator.VALID_CATEGORIES.has('reputation')).toBe(true); - // Note: 'other' removed in v1.0.0 for spec compliance (only 7 categories) + expect(errors).toHaveLength(0); + expect(report.protocol).toBe('smtp'); + expect(report.smtp_from).toBe('spammer@evil.example.com'); + expect(report.smtp_to).toBe('victim@example.com'); + expect(report.subject).toBe('You won!'); + expect(report.message_id).toBe('<123456@evil.example.com>'); }); - it('should have event types for all categories', () => { - const categories = Array.from(XARFGenerator.VALID_CATEGORIES); - categories.forEach((category) => { - expect(XARFGenerator.EVENT_TYPES[category]).toBeDefined(); - expect(Array.isArray(XARFGenerator.EVENT_TYPES[category])).toBe(true); + it('should preserve content-specific fields', () => { + const { report, errors } = createReport({ + ...baseInput, + category: 'content', + type: 'phishing', + url: 'http://malicious.example.com', + domain: 'malicious.example.com', + target_brand: 'Example Bank', + verification_method: 'manual', }); + + expect(errors).toHaveLength(0); + expect(report.url).toBe('http://malicious.example.com'); + expect(report.domain).toBe('malicious.example.com'); + expect(report.target_brand).toBe('Example Bank'); + expect(report.verification_method).toBe('manual'); + }); + + it('should preserve additional/custom fields without dropping them', () => { + const { report, warnings } = createReport({ + ...baseInput, + category: 'connection', + type: 'ddos', + destination_ip: '203.0.113.10', + protocol: 'tcp', + first_seen: '2024-01-15T09:00:00Z', + source_port: 12345, + custom_field: 'custom_value', + } as any); + + expect((report as any).custom_field).toBe('custom_value'); + // Unknown fields produce warnings but are still preserved + expect(warnings.some((w) => w.field === 'custom_field')).toBe(true); }); }); }); diff --git a/tests/generator.union-types.test.ts b/tests/generator.union-types.test.ts deleted file mode 100644 index bd9d031..0000000 --- a/tests/generator.union-types.test.ts +++ /dev/null @@ -1,307 +0,0 @@ -/** - * Tests for discriminated union types in GeneratorOptions - * - * These tests verify that category-specific fields can be passed directly - * to generateReport() instead of using additionalFields, providing better - * TypeScript type safety and autocomplete. - */ - -import { XARFGenerator } from '../src/generator'; -import type { - ContentGeneratorOptions, - ConnectionGeneratorOptions, - MessagingGeneratorOptions, -} from '../src/generator'; - -describe('GeneratorOptions Union Types', () => { - let generator: XARFGenerator; - - const baseOptions = { - reporter: { - org: 'Test Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test Org', - contact: 'abuse@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - }; - - beforeEach(() => { - generator = new XARFGenerator(); - }); - - describe('Content category with direct url field', () => { - it('should accept url as a direct field', () => { - const options: ContentGeneratorOptions = { - ...baseOptions, - category: 'content', - type: 'phishing', - url: 'http://malicious.example.com', - }; - - const report = generator.generateReport(options); - - expect(report.category).toBe('content'); - expect(report.url).toBe('http://malicious.example.com'); - }); - - it('should accept multiple content-specific fields directly', () => { - const options: ContentGeneratorOptions = { - ...baseOptions, - category: 'content', - type: 'phishing', - url: 'http://malicious.example.com', - domain: 'malicious.example.com', - target_brand: 'Example Bank', - verification_method: 'manual', - }; - - const report = generator.generateReport(options); - - expect(report.url).toBe('http://malicious.example.com'); - expect(report.domain).toBe('malicious.example.com'); - expect(report.target_brand).toBe('Example Bank'); - expect(report.verification_method).toBe('manual'); - }); - - it('should still work with additionalFields for backward compatibility', () => { - const report = generator.generateReport({ - ...baseOptions, - category: 'content', - type: 'phishing', - additionalFields: { - url: 'http://legacy.example.com', - }, - }); - - expect(report.url).toBe('http://legacy.example.com'); - }); - - it('should allow additionalFields to override direct fields', () => { - const options: ContentGeneratorOptions = { - ...baseOptions, - category: 'content', - type: 'phishing', - url: 'http://direct.example.com', - additionalFields: { - url: 'http://override.example.com', - }, - }; - - const report = generator.generateReport(options); - - expect(report.url).toBe('http://override.example.com'); - }); - }); - - describe('Connection category with direct fields', () => { - it('should accept destination_ip and protocol as direct fields', () => { - const options: ConnectionGeneratorOptions = { - ...baseOptions, - category: 'connection', - type: 'ddos', - destination_ip: '203.0.113.10', - protocol: 'tcp', - first_seen: '2024-01-15T09:00:00Z', - source_port: 12345, - }; - - const report = generator.generateReport(options); - - expect(report.category).toBe('connection'); - expect(report.destination_ip).toBe('203.0.113.10'); - expect(report.protocol).toBe('tcp'); - }); - - it('should accept optional connection fields', () => { - const options: ConnectionGeneratorOptions = { - ...baseOptions, - category: 'connection', - type: 'ddos', - destination_ip: '203.0.113.10', - protocol: 'tcp', - first_seen: '2024-01-15T09:00:00Z', - source_port: 12345, - destination_port: 80, - attack_vector: 'syn_flood', - peak_pps: 1000000, - }; - - const report = generator.generateReport(options); - - expect(report.destination_port).toBe(80); - expect(report.attack_vector).toBe('syn_flood'); - expect(report.peak_pps).toBe(1000000); - }); - - it('should still work with additionalFields for backward compatibility', () => { - const report = generator.generateReport({ - ...baseOptions, - category: 'connection', - type: 'ddos', - additionalFields: { - destination_ip: '203.0.113.20', - protocol: 'udp', - first_seen: '2024-01-15T09:00:00Z', - source_port: 12345, - }, - }); - - expect(report.destination_ip).toBe('203.0.113.20'); - expect(report.protocol).toBe('udp'); - }); - }); - - describe('Messaging category with direct fields', () => { - it('should accept protocol and smtp fields directly', () => { - const options: MessagingGeneratorOptions = { - ...baseOptions, - category: 'messaging', - type: 'spam', - protocol: 'smtp', - smtp_from: 'spammer@evil.example.com', - source_port: 25, - subject: 'You won!', - }; - - const report = generator.generateReport(options); - - expect(report.category).toBe('messaging'); - expect(report.protocol).toBe('smtp'); - expect(report.smtp_from).toBe('spammer@evil.example.com'); - expect(report.subject).toBe('You won!'); - }); - - it('should accept optional messaging fields', () => { - const options: MessagingGeneratorOptions = { - ...baseOptions, - category: 'messaging', - type: 'spam', - protocol: 'smtp', - smtp_from: 'spammer@evil.example.com', - source_port: 25, - smtp_to: 'victim@example.com', - subject: 'You won!', - message_id: '<123456@evil.example.com>', - }; - - const report = generator.generateReport(options); - - expect(report.smtp_to).toBe('victim@example.com'); - expect(report.message_id).toBe('<123456@evil.example.com>'); - }); - }); - - describe('Mixed direct fields and additionalFields', () => { - it('should merge direct fields with additionalFields', () => { - const options: ConnectionGeneratorOptions = { - ...baseOptions, - category: 'connection', - type: 'ddos', - destination_ip: '203.0.113.10', - protocol: 'tcp', - first_seen: '2024-01-15T09:00:00Z', - source_port: 12345, - additionalFields: { - destination_port: 443, - peak_pps: 1000000, - }, - }; - - const report = generator.generateReport(options); - - expect(report.destination_ip).toBe('203.0.113.10'); - expect(report.protocol).toBe('tcp'); - expect(report.destination_port).toBe(443); - expect(report.peak_pps).toBe(1000000); - }); - - it('should reject unknown fields in additionalFields', () => { - const options: ConnectionGeneratorOptions = { - ...baseOptions, - category: 'connection', - type: 'ddos', - destination_ip: '203.0.113.10', - protocol: 'tcp', - additionalFields: { - custom_field: 'custom_value', - }, - }; - - expect(() => generator.generateReport(options)).toThrow(/custom_field.*Unknown field/); - }); - }); - - describe('Type safety with discriminated unions', () => { - it('should provide type-safe access to category-specific fields', () => { - // This test verifies TypeScript compile-time type checking - // The ContentGeneratorOptions type enforces url is available - const contentOptions: ContentGeneratorOptions = { - ...baseOptions, - category: 'content', - type: 'phishing', - url: 'http://test.example.com', - }; - - // The ConnectionGeneratorOptions type enforces destination_ip and protocol are available - const connectionOptions: ConnectionGeneratorOptions = { - ...baseOptions, - category: 'connection', - type: 'ddos', - destination_ip: '192.0.2.1', - protocol: 'tcp', - first_seen: '2024-01-15T09:00:00Z', - source_port: 12345, - }; - - const contentReport = generator.generateReport(contentOptions); - const connectionReport = generator.generateReport(connectionOptions); - - expect(contentReport.url).toBeDefined(); - expect(connectionReport.destination_ip).toBeDefined(); - expect(connectionReport.protocol).toBeDefined(); - }); - }); - - describe('Schema-derived field extraction', () => { - it('should extract fields from schema registry for content category', () => { - // Fields defined in content-base.json should be extracted - const options: ContentGeneratorOptions = { - ...baseOptions, - category: 'content', - type: 'malware', - url: 'http://malware.example.com', - verified_at: '2024-01-15T10:00:00Z', - hosting_provider: 'Example Host', - }; - - const report = generator.generateReport(options); - - expect(report.url).toBe('http://malware.example.com'); - expect(report.verified_at).toBe('2024-01-15T10:00:00Z'); - expect(report.hosting_provider).toBe('Example Host'); - }); - - it('should extract fields from schema registry for messaging category', () => { - const options: MessagingGeneratorOptions = { - ...baseOptions, - category: 'messaging', - type: 'spam', - protocol: 'smtp', - smtp_from: 'spam@evil.example.com', - source_port: 25, - sender_name: 'Nigerian Prince', - }; - - const report = generator.generateReport(options); - - expect(report.protocol).toBe('smtp'); - expect(report.smtp_from).toBe('spam@evil.example.com'); - expect(report.sender_name).toBe('Nigerian Prince'); - }); - }); -}); diff --git a/tests/parser.edge-cases.test.ts b/tests/parser.edge-cases.test.ts deleted file mode 100644 index b2a17f2..0000000 --- a/tests/parser.edge-cases.test.ts +++ /dev/null @@ -1,378 +0,0 @@ -/** - * Edge Case Tests for XARF Parser - */ - -import { XARFParser } from '../src/parser'; -import { XARFParseError, XARFValidationError } from '../src/errors'; - -describe('XARFParser Edge Cases', () => { - describe('parse error handling', () => { - it('should handle parse error when category is unsupported in strict mode', () => { - const reportData = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'invalid_category', - type: 'botnet', - evidence_source: 'honeypot', - }; - - const parser = new XARFParser(true); - - // Should throw XARFValidationError when category is invalid - // Schema validation catches this as an enum violation - expect(() => { - parser.parse(reportData); - }).toThrow(XARFValidationError); - }); - - it('should return data when try-catch has error in non-strict mode', () => { - const reportData = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'content', - type: 'unclassified', - evidence_source: 'manual_analysis', - }; - - const parser = new XARFParser(false); - const report = parser.parse(reportData); - - expect(report.category).toBe('content'); - }); - - it('should handle invalid JSON string parse error', () => { - const parser = new XARFParser(); - - expect(() => { - parser.parse('{"invalid": json}'); - }).toThrow(XARFParseError); - expect(() => { - parser.parse('{"invalid": json}'); - }).toThrow('Invalid JSON'); - }); - - it('should handle reporter not being an object', () => { - const invalidData = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: null, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'messaging', - type: 'spam', - evidence_source: 'spamtrap', - }; - - const parser = new XARFParser(false); - const result = parser.validate(invalidData); - - expect(result).toBe(false); - expect(parser.getErrors().some((e) => e.includes('reporter is required'))).toBe(true); - }); - - it('should handle invalid timestamp format gracefully', () => { - const invalidData = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: 'invalid-timestamp-format', - reporter: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'messaging', - type: 'spam', - evidence_source: 'spamtrap', - }; - - const parser = new XARFParser(false); - const report = parser.parse(invalidData); - - // Parser accepts the data but validator would catch it - expect(report.category).toBe('messaging'); - }); - - it('should validate JSON string input', () => { - const invalidData = { - xarf_version: '3.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'messaging', - type: 'spam', - evidence_source: 'spamtrap', - }; - - const parser = new XARFParser(false); - const result = parser.validate(JSON.stringify(invalidData)); - - expect(result).toBe(false); - expect(parser.getErrors().some((e) => e.includes('Unsupported XARF version'))).toBe(true); - }); - - it('should handle validate with invalid JSON string', () => { - const parser = new XARFParser(false); - const result = parser.validate('invalid json string'); - - expect(result).toBe(false); - expect(parser.getErrors().some((e) => e.includes('Invalid JSON'))).toBe(true); - }); - }); - - describe('category-specific edge cases', () => { - it('should validate messaging with protocol smtp but no subject for social_engineering', () => { - const reportData = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'messaging', - type: 'bulk_messaging', - evidence_source: 'spamtrap', - protocol: 'smtp', - smtp_from: 'sender@example.com', - }; - - const parser = new XARFParser(false); - const result = parser.validate(reportData); - - // Should pass because subject is only required for spam - expect(result).toBe(true); - }); - - it('should require subject for spam with smtp protocol', () => { - const invalidData = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'messaging', - type: 'spam', - evidence_source: 'spamtrap', - protocol: 'smtp', - smtp_from: 'spammer@example.com', - }; - - const parser = new XARFParser(false); - const result = parser.validate(invalidData); - - expect(result).toBe(false); - expect(parser.getErrors().some((e) => e.includes('subject required'))).toBe(true); - }); - - it('should reject invalid messaging type', () => { - const invalidData = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'messaging', - type: 'invalid_type', - evidence_source: 'spamtrap', - protocol: 'smtp', - smtp_from: 'test@example.com', - }; - - const parser = new XARFParser(false); - const result = parser.validate(invalidData); - - expect(result).toBe(false); - expect(parser.getErrors().some((e) => e.includes('Invalid messaging type'))).toBe(true); - }); - - it('should validate connection report missing protocol', () => { - const invalidData = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'connection', - type: 'ddos', - evidence_source: 'honeypot', - destination_ip: '203.0.113.1', - }; - - const parser = new XARFParser(false); - const result = parser.validate(invalidData); - - expect(result).toBe(false); - expect(parser.getErrors().some((e) => e.includes('protocol required'))).toBe(true); - }); - - it('should validate invalid connection type', () => { - const invalidData = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'connection', - type: 'invalid_connection_type', - evidence_source: 'honeypot', - destination_ip: '203.0.113.1', - protocol: 'tcp', - }; - - const parser = new XARFParser(false); - const result = parser.validate(invalidData); - - expect(result).toBe(false); - expect(parser.getErrors().some((e) => e.includes('Invalid connection type'))).toBe(true); - }); - - it('should validate invalid content type', () => { - const invalidData = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'content', - type: 'invalid_content_type', - evidence_source: 'user_report', - url: 'http://example.com', - }; - - const parser = new XARFParser(false); - const result = parser.validate(invalidData); - - expect(result).toBe(false); - expect(parser.getErrors().some((e) => e.includes('Invalid content type'))).toBe(true); - }); - }); - - describe('reporter validation edge cases', () => { - it('should detect missing reporter fields', () => { - const invalidData = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'messaging', - type: 'spam', - evidence_source: 'spamtrap', - }; - - const parser = new XARFParser(false); - const result = parser.validate(invalidData); - - expect(result).toBe(false); - expect( - parser - .getErrors() - .some((e) => e.includes('reporter.contact') || e.includes('reporter.domain')) - ).toBe(true); - }); - }); -}); diff --git a/tests/parser.test.ts b/tests/parser.test.ts index cf7f5d7..0ed2f12 100644 --- a/tests/parser.test.ts +++ b/tests/parser.test.ts @@ -2,360 +2,276 @@ * Tests for XARF Parser */ -import { XARFParser } from '../src/parser'; -import { XARFParseError, XARFValidationError } from '../src/errors'; +import { parse } from '../src/parser'; +import { XARFParseError } from '../src/errors'; import type { MessagingReport, ConnectionReport, ContentReport } from '../src/types'; -describe('XARFParser', () => { - describe('parse', () => { - it('should parse valid messaging report', () => { - const reportData = { - xarf_version: '4.0.0', - report_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.100', - category: 'messaging', - type: 'spam', - evidence_source: 'spamtrap', - protocol: 'smtp', - smtp_from: 'spammer@example.com', - subject: 'Test Spam', - }; - - const parser = new XARFParser(); - const report = parser.parse(reportData) as MessagingReport; - - expect(report.category).toBe('messaging'); - expect(report.type).toBe('spam'); - expect(report.smtp_from).toBe('spammer@example.com'); +const validMessagingReport = { + xarf_version: '4.2.0', + report_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + timestamp: '2024-01-15T10:30:00Z', + reporter: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + sender: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + source_identifier: '192.0.2.100', + category: 'messaging', + type: 'spam', + evidence_source: 'spamtrap', + protocol: 'smtp', + smtp_from: 'spammer@example.com', + source_port: 25, + subject: 'Test Spam', +}; + +const validConnectionReport = { + xarf_version: '4.2.0', + report_id: 'b2c3d4e5-f6a7-8901-bcde-f1234567890a', + timestamp: '2024-01-15T11:00:00Z', + reporter: { + org: 'Security Monitor', + contact: 'security@example.com', + domain: 'example.com', + }, + sender: { + org: 'Security Monitor', + contact: 'security@example.com', + domain: 'example.com', + }, + source_identifier: '192.0.2.200', + source_port: 12345, + category: 'connection', + type: 'ddos', + evidence_source: 'honeypot', + destination_ip: '203.0.113.10', + protocol: 'tcp', + first_seen: '2025-12-16T07:00:00.000Z', +}; + +const validContentReport = { + xarf_version: '4.2.0', + report_id: 'c3d4e5f6-a7b8-9012-cdef-234567890abc', + timestamp: '2024-01-15T12:00:00Z', + reporter: { + org: 'Web Security', + contact: 'web@example.com', + domain: 'example.com', + }, + sender: { + org: 'Web Security', + contact: 'web@example.com', + domain: 'example.com', + }, + source_identifier: '192.0.2.50', + category: 'content', + type: 'phishing', + evidence_source: 'user_report', + url: 'http://phishing.example.com', +}; + +describe('parse', () => { + describe('valid reports', () => { + it('should parse messaging report and cast to MessagingReport', () => { + const { report, errors } = parse(validMessagingReport); + const messaging = report as MessagingReport; + + expect(errors).toHaveLength(0); + expect(messaging.category).toBe('messaging'); + expect(messaging.type).toBe('spam'); + expect(messaging.smtp_from).toBe('spammer@example.com'); }); - it('should parse valid connection report', () => { - const reportData = { - xarf_version: '4.0.0', - report_id: 'b2c3d4e5-f6g7-8901-bcde-f1234567890a', - timestamp: '2024-01-15T11:00:00Z', - reporter: { - org: 'Security Monitor', - contact: 'security@example.com', - domain: 'example.com', - }, - sender: { - org: 'Security Monitor', - contact: 'security@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.200', - category: 'connection', - type: 'ddos', - evidence_source: 'honeypot', - destination_ip: '203.0.113.10', - protocol: 'tcp', - destination_port: 80, - attack_type: 'syn_flood', - }; - - const parser = new XARFParser(); - const report = parser.parse(reportData) as ConnectionReport; + it('should parse connection report and cast to ConnectionReport', () => { + const { report, errors } = parse(validConnectionReport); + const connection = report as ConnectionReport; + expect(errors).toHaveLength(0); + expect(connection.category).toBe('connection'); + expect(connection.type).toBe('ddos'); + expect(connection.destination_ip).toBe('203.0.113.10'); + }); + + it('should parse content report and cast to ContentReport', () => { + const { report, errors } = parse(validContentReport); + const content = report as ContentReport; + + expect(errors).toHaveLength(0); + expect(content.category).toBe('content'); + expect(content.type).toBe('phishing'); + expect(content.url).toBe('http://phishing.example.com'); + }); + + it('should accept JSON string input', () => { + const { report, errors } = parse(JSON.stringify(validConnectionReport)); + + expect(errors).toHaveLength(0); expect(report.category).toBe('connection'); - expect(report.type).toBe('ddos'); - expect(report.destination_ip).toBe('203.0.113.10'); }); - it('should parse valid content report', () => { - const reportData = { - xarf_version: '4.0.0', - report_id: 'c3d4e5f6-g7h8-9012-cdef-234567890abc', - timestamp: '2024-01-15T12:00:00Z', - reporter: { - org: 'Web Security', - contact: 'web@example.com', - domain: 'example.com', - }, - sender: { - org: 'Web Security', - contact: 'web@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.300', - category: 'content', - type: 'phishing', - evidence_source: 'user_report', - url: 'http://phishing.example.com', - }; - - const parser = new XARFParser(); - const report = parser.parse(reportData) as ContentReport; - - expect(report.category).toBe('content'); - expect(report.type).toBe('phishing'); - expect(report.url).toBe('http://phishing.example.com'); + it('should accept spam without subject (recommended, not required)', () => { + const data = { ...validMessagingReport } as any; + delete data.subject; + + const { errors } = parse(data); + + expect(errors).toHaveLength(0); }); + }); - it('should parse from JSON string', () => { - const reportData = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'messaging', - type: 'spam', - evidence_source: 'spamtrap', - }; - - const parser = new XARFParser(); - const report = parser.parse(JSON.stringify(reportData)); - - expect(report.category).toBe('messaging'); - expect(report.type).toBe('spam'); + describe('JSON parsing errors', () => { + it('should throw XARFParseError for malformed JSON string', () => { + expect(() => parse('{"invalid": json}')).toThrow(XARFParseError); + expect(() => parse('{"invalid": json}')).toThrow('Invalid JSON'); }); - it('should throw error for invalid JSON string', () => { - const parser = new XARFParser(); + it('should throw XARFParseError for non-JSON string', () => { + expect(() => parse('invalid json string')).toThrow(XARFParseError); + }); + }); - expect(() => { - parser.parse('{invalid json}'); - }).toThrow(XARFParseError); + describe('validation errors', () => { + it('should return errors for invalid xarf_version', () => { + const { errors } = parse({ ...validMessagingReport, xarf_version: '3.0.0' }); + + expect(errors.length).toBeGreaterThan(0); + expect(errors.some((e) => e.includes('xarf_version'))).toBe(true); }); - it('should throw validation error in strict mode', () => { - const invalidData = { - xarf_version: '4.0.0', - // Missing required fields - }; + it('should return errors for invalid xarf_version in JSON string input', () => { + const data = JSON.stringify({ ...validMessagingReport, xarf_version: '3.0.0' }); + const { errors } = parse(data); + + expect(errors.length).toBeGreaterThan(0); + expect(errors.some((e) => e.includes('xarf_version'))).toBe(true); + }); - const parser = new XARFParser(true); + it('should return errors for missing required fields', () => { + const { errors } = parse({ xarf_version: '4.2.0' }); - expect(() => { - parser.parse(invalidData); - }).toThrow(XARFValidationError); + expect(errors.length).toBeGreaterThan(0); + expect(errors.some((e) => e.includes('required'))).toBe(true); }); - }); - describe('validate', () => { - it('should return false for invalid version', () => { - const invalidData = { - xarf_version: '3.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'messaging', - type: 'spam', - evidence_source: 'spamtrap', - }; - - const parser = new XARFParser(false); - const result = parser.validate(invalidData); - - expect(result).toBe(false); - const errors = parser.getErrors(); + it('should return errors for invalid reporter contact email', () => { + const { errors } = parse({ + ...validMessagingReport, + reporter: { org: 'Test', contact: 'invalid-email', domain: 'example.com' }, + }); + + expect(errors.length).toBeGreaterThan(0); + expect( + errors.some((e) => e.includes('valid email address') || e.includes('reporter.contact')) + ).toBe(true); + }); + + it('should return errors for null reporter', () => { + const { errors } = parse({ ...validMessagingReport, reporter: null }); + expect(errors.length).toBeGreaterThan(0); - expect(errors[0]).toContain('Unsupported XARF version'); + expect(errors.some((e) => e.includes('reporter'))).toBe(true); }); - it('should return false for missing required fields', () => { - const invalidData = { - xarf_version: '4.0.0', - // Missing most required fields - }; + it('should return errors for missing reporter.contact and reporter.domain', () => { + const { errors } = parse({ + ...validMessagingReport, + reporter: { org: 'Test' }, + }); + + expect(errors.length).toBeGreaterThan(0); + expect(errors.some((e) => e.includes('reporter') && e.includes('required'))).toBe(true); + }); - const parser = new XARFParser(false); - const result = parser.validate(invalidData); + it('should return errors for invalid timestamp format', () => { + const { report, errors } = parse({ + ...validMessagingReport, + timestamp: 'invalid-timestamp-format', + }); - expect(result).toBe(false); - const errors = parser.getErrors(); - expect(errors.some((e) => e.includes('Missing required fields'))).toBe(true); + // Non-strict: returns the data despite the invalid timestamp + expect(report.timestamp).toBe('invalid-timestamp-format'); + expect(errors.some((e) => e.includes('timestamp'))).toBe(true); }); - it('should return false for invalid reporter contact', () => { - const invalidData = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test', - contact: 'invalid-email', - domain: 'example.com', - }, - sender: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'messaging', - type: 'spam', - evidence_source: 'spamtrap', - }; - - const parser = new XARFParser(false); - const result = parser.validate(invalidData); - - expect(result).toBe(false); - const errors = parser.getErrors(); - expect(errors.some((e) => e.includes('Invalid email format'))).toBe(true); + it('should return errors in strict mode for missing fields', () => { + const { errors } = parse({ xarf_version: '4.2.0' }, { strict: true }); + + expect(errors.length).toBeGreaterThan(0); }); + }); - it('should handle unsupported category', () => { - const reportData = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', + describe('category and type validation', () => { + it('should return errors for invalid category', () => { + const { report, errors } = parse({ + ...validMessagingReport, category: 'invalid_category', type: 'test', - evidence_source: 'honeypot', - }; - - const parser = new XARFParser(false); - const report = parser.parse(reportData); + }); - expect(report.category).toBe('invalid_category'); - const errors = parser.getErrors(); expect(errors.length).toBeGreaterThan(0); - // Schema validation catches invalid category as enum violation expect(errors.some((e) => e.includes('category'))).toBe(true); + // Report is still returned with original data + expect(report.category).toBe('invalid_category'); }); - }); - describe('category-specific validation', () => { - it('should validate messaging reports', () => { - const invalidMessaging = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'messaging', + it('should return errors for unknown type within valid category', () => { + const { errors } = parse({ + ...validMessagingReport, type: 'invalid_type', - evidence_source: 'spamtrap', - }; - - const parser = new XARFParser(false); - const result = parser.validate(invalidMessaging); + }); - expect(result).toBe(false); - expect(parser.getErrors().some((e) => e.includes('Invalid messaging type'))).toBe(true); + expect(errors.length).toBeGreaterThan(0); }); - it('should validate connection reports require destination_ip', () => { - const invalidConnection = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'connection', - type: 'ddos', - evidence_source: 'honeypot', - protocol: 'tcp', - }; - - const parser = new XARFParser(false); - const result = parser.validate(invalidConnection); - - expect(result).toBe(false); - expect(parser.getErrors().some((e) => e.includes('destination_ip required'))).toBe(true); + it('should require protocol for connection reports', () => { + const data = { ...validConnectionReport } as any; + delete data.protocol; + + const { errors } = parse(data); + + expect(errors.length).toBeGreaterThan(0); + expect(errors.some((e) => e.includes('protocol') && e.includes('required'))).toBe(true); }); - it('should validate content reports require url', () => { - const invalidContent = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'content', - type: 'phishing', - evidence_source: 'user_report', - }; - - const parser = new XARFParser(false); - const result = parser.validate(invalidContent); - - expect(result).toBe(false); - expect(parser.getErrors().some((e) => e.includes('url required'))).toBe(true); + it('should accept bulk_messaging without subject', () => { + const data = { + ...validMessagingReport, + type: 'bulk_messaging', + evidence_source: 'automated_filter', + recipient_count: 5000, + } as any; + delete data.subject; + + const { errors } = parse(data); + + expect(errors).toHaveLength(0); }); }); - describe('getErrors', () => { - it('should return copy of errors array', () => { - const parser = new XARFParser(false); - parser.validate({}); + describe('warnings', () => { + it('should warn about unknown fields', () => { + const { warnings } = parse({ + ...validContentReport, + severety: 'high', + sourcePort: 443, + }); + + expect(warnings.length).toBeGreaterThan(0); + expect(warnings.some((w) => w.includes('severety') || w.includes('unknown'))).toBe(true); + }); - const errors1 = parser.getErrors(); - const errors2 = parser.getErrors(); + it('should warn about camelCase field names (not in XARF spec)', () => { + const { warnings } = parse({ + ...validContentReport, + contentType: 'text/html', + }); - expect(errors1).toEqual(errors2); - expect(errors1).not.toBe(errors2); // Different array instances + expect(warnings.some((w) => w.includes('contentType') || w.includes('unknown'))).toBe(true); }); }); }); diff --git a/tests/schema-registry.test.ts b/tests/schema-registry.test.ts index 15b5d2a..ff526de 100644 --- a/tests/schema-registry.test.ts +++ b/tests/schema-registry.test.ts @@ -115,52 +115,6 @@ describe('SchemaRegistry', () => { }); }); - describe('getEvidenceSources', () => { - it('should return valid evidence sources', () => { - const sources = registry.getEvidenceSources(); - - expect(sources.size).toBeGreaterThan(0); - expect(sources.has('spamtrap')).toBe(true); - expect(sources.has('honeypot')).toBe(true); - expect(sources.has('user_report')).toBe(true); - }); - }); - - describe('isValidEvidenceSource', () => { - it('should return true for valid sources', () => { - expect(registry.isValidEvidenceSource('spamtrap')).toBe(true); - expect(registry.isValidEvidenceSource('honeypot')).toBe(true); - }); - - it('should return false for invalid sources', () => { - expect(registry.isValidEvidenceSource('invalid_source')).toBe(false); - }); - }); - - describe('getSeverities', () => { - it('should return valid severity levels', () => { - const severities = registry.getSeverities(); - - expect(severities.size).toBe(4); - expect(severities.has('low')).toBe(true); - expect(severities.has('medium')).toBe(true); - expect(severities.has('high')).toBe(true); - expect(severities.has('critical')).toBe(true); - }); - }); - - describe('isValidSeverity', () => { - it('should return true for valid severities', () => { - expect(registry.isValidSeverity('low')).toBe(true); - expect(registry.isValidSeverity('critical')).toBe(true); - }); - - it('should return false for invalid severities', () => { - expect(registry.isValidSeverity('extreme')).toBe(false); - expect(registry.isValidSeverity('')).toBe(false); - }); - }); - describe('getRequiredFields', () => { it('should return required fields from core schema', () => { const required = registry.getRequiredFields(); diff --git a/tests/schema-validation.test.ts b/tests/schema-validation.test.ts index 09febf6..931d1e7 100644 --- a/tests/schema-validation.test.ts +++ b/tests/schema-validation.test.ts @@ -1,95 +1,46 @@ /** - * JSON Schema Validation Tests + * Tests for SchemaValidator * - * Tests comprehensive JSON schema validation and compares with hand-coded validator. - * Ensures schema validation catches all errors and validates backward compatibility. + * Tests JSON schema validation against the official XARF v4 schemas. */ import { SchemaValidator } from '../src/schema-validator'; import { XARFValidator } from '../src/validator'; -import type { XARFReport, XARFCategory } from '../src/types'; +import type { XARFReport } from '../src/types'; +import { validReports, getReport } from './fixtures'; -describe('JSON Schema Validation', () => { +describe('SchemaValidator', () => { let schemaValidator: SchemaValidator; - let handCodedValidator: XARFValidator; beforeEach(() => { schemaValidator = new SchemaValidator(); - // Disable schema validation to test hand-coded validation alone - handCodedValidator = new XARFValidator(false); }); - const createValidReport = (category: XARFCategory = 'connection'): XARFReport => ({ - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T14:30:25Z', - reporter: { - org: 'Security Corp', - contact: 'abuse@security.com', - domain: 'security.com', - }, - sender: { - org: 'Security Corp', - contact: 'abuse@security.com', - domain: 'security.com', - }, - source_identifier: '192.0.2.100', - category, - type: category === 'messaging' ? 'spam' : category === 'connection' ? 'ddos' : 'phishing', - evidence_source: 'honeypot', - ...(category === 'connection' && { - destination_ip: '203.0.113.10', - protocol: 'tcp', - }), - ...(category === 'content' && { - url: 'http://example.com', - }), - ...(category === 'messaging' && { - protocol: 'smtp', - smtp_from: 'spammer@evil.com', - }), - }); - - describe('1. Valid reports pass schema validation', () => { - it('should validate basic connection report', () => { - const report = createValidReport('connection'); - const result = schemaValidator.validateCore(report); - - expect(result.valid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - - it('should validate messaging report', () => { - const report = createValidReport('messaging'); - const result = schemaValidator.validateCore(report); - - expect(result.valid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - - it('should validate content report', () => { - const report = createValidReport('content'); - const result = schemaValidator.validateCore(report); + describe('valid reports — all 32 category/type combinations', () => { + it.each(Object.keys(validReports))('should validate %s', (key) => { + const result = schemaValidator.validate(validReports[key]); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); + }); + describe('valid reports — optional and evidence fields', () => { it('should validate report with optional fields', () => { - const report = createValidReport('connection'); + const report = getReport('connection/ddos'); report.description = 'DDoS attack description'; report.severity = 'high'; report.confidence = 0.95; report.tags = ['malware:botnet', 'attack:ddos']; - const result = schemaValidator.validateCore(report); + const result = schemaValidator.validate(report); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); it('should validate report with evidence array', () => { - const report = createValidReport('connection'); + const report = getReport('connection/ddos'); report.evidence = [ { content_type: 'text/plain', @@ -99,107 +50,106 @@ describe('JSON Schema Validation', () => { }, ]; - const result = schemaValidator.validateCore(report); + const result = schemaValidator.validate(report); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); }); - describe('2. Invalid reports fail with proper errors', () => { + describe('invalid reports', () => { it('should fail when missing required field (report_id)', () => { - const report = createValidReport(); + const report = getReport('connection/ddos'); delete (report as any).report_id; - const result = schemaValidator.validateCore(report); + const result = schemaValidator.validate(report); expect(result.valid).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); expect(result.errors.some((e: string) => e.includes('report_id'))).toBe(true); }); - it('should fail when xarf_version has wrong format', () => { - const report = createValidReport(); + it('should fail when xarf_version has wrong value', () => { + const report = getReport('connection/ddos'); report.xarf_version = '3.0.0'; - const result = schemaValidator.validateCore(report); + const result = schemaValidator.validate(report); expect(result.valid).toBe(false); expect(result.errors.some((e: string) => e.includes('xarf_version'))).toBe(true); }); it('should fail when report_id is not a UUID', () => { - const report = createValidReport(); + const report = getReport('connection/ddos'); report.report_id = 'not-a-uuid'; - const result = schemaValidator.validateCore(report); + const result = schemaValidator.validate(report); expect(result.valid).toBe(false); expect(result.errors.some((e: string) => e.includes('report_id'))).toBe(true); }); it('should fail when timestamp is not ISO 8601', () => { - const report = createValidReport(); + const report = getReport('connection/ddos'); report.timestamp = '2024-01-15 10:30:00'; - const result = schemaValidator.validateCore(report); + const result = schemaValidator.validate(report); expect(result.valid).toBe(false); expect(result.errors.some((e: string) => e.includes('timestamp'))).toBe(true); }); it('should fail when reporter.contact is not an email', () => { - const report = createValidReport(); + const report = getReport('connection/ddos'); report.reporter.contact = 'not-an-email'; - const result = schemaValidator.validateCore(report); + const result = schemaValidator.validate(report); expect(result.valid).toBe(false); expect(result.errors.some((e: string) => e.includes('reporter/contact'))).toBe(true); }); it('should fail when reporter.domain is not a hostname', () => { - const report = createValidReport(); + const report = getReport('connection/ddos'); report.reporter.domain = 'invalid domain with spaces'; - const result = schemaValidator.validateCore(report); + const result = schemaValidator.validate(report); expect(result.valid).toBe(false); expect(result.errors.some((e: string) => e.includes('reporter/domain'))).toBe(true); }); it('should fail when category is invalid', () => { - const report = createValidReport(); + const report = getReport('connection/ddos'); (report as any).category = 'invalid_category'; - const result = schemaValidator.validateCore(report); + const result = schemaValidator.validate(report); expect(result.valid).toBe(false); expect(result.errors.some((e: string) => e.includes('category'))).toBe(true); }); it('should fail when confidence is out of range', () => { - const report = createValidReport(); + const report = getReport('connection/ddos'); report.confidence = 1.5; - const result = schemaValidator.validateCore(report); + const result = schemaValidator.validate(report); expect(result.valid).toBe(false); expect(result.errors.some((e: string) => e.includes('confidence'))).toBe(true); }); it('should fail when tags have wrong format', () => { - const report = createValidReport(); + const report = getReport('connection/ddos'); report.tags = ['invalid-tag-without-colon']; - const result = schemaValidator.validateCore(report); + const result = schemaValidator.validate(report); expect(result.valid).toBe(false); expect(result.errors.some((e: string) => e.includes('tags'))).toBe(true); }); it('should fail when evidence hash has wrong format', () => { - const report = createValidReport(); + const report = getReport('connection/ddos'); report.evidence = [ { content_type: 'text/plain', @@ -209,78 +159,42 @@ describe('JSON Schema Validation', () => { }, ]; - const result = schemaValidator.validateCore(report); + const result = schemaValidator.validate(report); expect(result.valid).toBe(false); expect(result.errors.some((e: string) => e.includes('hash'))).toBe(true); }); }); - describe('3. All 7 categories with schema validation', () => { - const categories: XARFCategory[] = [ - 'messaging', - 'connection', - 'content', - 'infrastructure', - 'copyright', - 'vulnerability', - 'reputation', - ]; - - categories.forEach((category) => { - it(`should validate ${category} category against schema`, () => { - const report = createValidReport(category); - const result = schemaValidator.validateCore(report); - - expect(result.valid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - - it(`should fail ${category} category when missing required fields`, () => { - const report: any = { - category, - type: 'test', - }; - - const result = schemaValidator.validateCore(report); - - expect(result.valid).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); + describe('constraint violations', () => { + let validator: XARFValidator; + + beforeEach(() => { + validator = new XARFValidator(); }); - }); - describe('4. Schema validation catches errors hand-coded validator misses', () => { - it('should catch when description exceeds maxLength', () => { - const report = createValidReport(); + it('should reject description exceeding maxLength', () => { + const report = getReport('connection/ddos'); report.description = 'x'.repeat(1001); - const schemaResult = schemaValidator.validateCore(report); - const handCodedResult = handCodedValidator.validate(report); + const result = validator.validate(report); - // Schema catches length violation - expect(schemaResult.valid).toBe(false); - expect(schemaResult.errors.some((e: string) => e.includes('description'))).toBe(true); - // Hand-coded validator doesn't check this - expect(handCodedResult.valid).toBe(true); + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.field === 'description')).toBe(true); }); - it('should catch when tags array exceeds maxItems', () => { - const report = createValidReport(); + it('should reject tags array exceeding maxItems', () => { + const report = getReport('connection/ddos'); report.tags = Array(21).fill('tag:value'); - const schemaResult = schemaValidator.validateCore(report); - const handCodedResult = handCodedValidator.validate(report); + const result = validator.validate(report); - // Schema catches array length violation - expect(schemaResult.valid).toBe(false); - expect(schemaResult.errors.some((e: string) => e.includes('tags'))).toBe(true); - // Hand-coded validator doesn't check this - expect(handCodedResult.valid).toBe(true); + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.field === 'tags')).toBe(true); }); - it('should catch when evidence array exceeds maxItems', () => { - const report = createValidReport(); + it('should reject evidence array exceeding maxItems', () => { + const report = getReport('connection/ddos'); report.evidence = Array(51) .fill(null) .map(() => ({ @@ -289,150 +203,54 @@ describe('JSON Schema Validation', () => { payload: 'dGVzdA==', })); - const schemaResult = schemaValidator.validateCore(report); - const handCodedResult = handCodedValidator.validate(report); + const result = validator.validate(report); - // Schema catches array length violation - expect(schemaResult.valid).toBe(false); - expect(schemaResult.errors.some((e: string) => e.includes('evidence'))).toBe(true); - // Hand-coded validator doesn't check this - expect(handCodedResult.valid).toBe(true); + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.field === 'evidence')).toBe(true); }); - it('should catch when reporter.org exceeds maxLength', () => { - const report = createValidReport(); + it('should reject reporter.org exceeding maxLength', () => { + const report = getReport('connection/ddos'); report.reporter.org = 'x'.repeat(201); - const schemaResult = schemaValidator.validateCore(report); - const handCodedResult = handCodedValidator.validate(report); + const result = validator.validate(report); - // Schema catches length violation - expect(schemaResult.valid).toBe(false); - expect(schemaResult.errors.some((e: string) => e.includes('reporter/org'))).toBe(true); - // Hand-coded validator doesn't check this - expect(handCodedResult.valid).toBe(true); + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.field.includes('reporter'))).toBe(true); }); - it('should catch when source_port is out of valid range', () => { - const report = createValidReport(); + it('should reject source_port out of valid range', () => { + const report = getReport('connection/ddos'); (report as any).source_port = 70000; - const schemaResult = schemaValidator.validateCore(report); - const handCodedResult = handCodedValidator.validate(report); + const result = validator.validate(report); - // Schema catches port range violation - expect(schemaResult.valid).toBe(false); - expect(schemaResult.errors.some((e: string) => e.includes('source_port'))).toBe(true); - // Hand-coded validator doesn't validate source_port - expect(handCodedResult.valid).toBe(true); + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.field === 'source_port')).toBe(true); }); - it('should catch additional properties in ContactInfo', () => { - const report = createValidReport(); + it('should reject additional properties in ContactInfo', () => { + const report = getReport('connection/ddos'); (report.reporter as any).extra_field = 'value'; - const schemaResult = schemaValidator.validateCore(report); - const handCodedResult = handCodedValidator.validate(report); - - // Schema has additionalProperties: false for ContactInfo - expect(schemaResult.valid).toBe(false); - expect(schemaResult.errors.some((e: string) => e.includes('reporter'))).toBe(true); - // Hand-coded allows additional properties - expect(handCodedResult.valid).toBe(true); - }); - }); - - describe('5. Backward compatibility - existing tests still pass', () => { - it('should validate reports that pass hand-coded validator', () => { - const report = createValidReport('connection'); - const handCodedResult = handCodedValidator.validate(report); + const result = validator.validate(report); - expect(handCodedResult.valid).toBe(true); - - const schemaResult = schemaValidator.validateCore(report); - - expect(schemaResult.valid).toBe(true); - }); - - it('should validate messaging report with all fields', () => { - const report: XARFReport = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T14:30:25Z', - reporter: { - org: 'Security Corp', - contact: 'abuse@security.com', - domain: 'security.com', - }, - sender: { - org: 'Security Corp', - contact: 'abuse@security.com', - domain: 'security.com', - }, - source_identifier: '192.0.2.100', - category: 'messaging', - type: 'spam', - evidence_source: 'spamtrap', - protocol: 'smtp', - smtp_from: 'spammer@evil.com', - smtp_to: 'victim@example.com', - subject: 'Get rich quick!', - }; - - const handCodedResult = handCodedValidator.validate(report); - expect(handCodedResult.valid).toBe(true); - - const schemaResult = schemaValidator.validateCore(report); - expect(schemaResult.valid).toBe(true); - }); - - it('should validate content report with URL', () => { - const report: XARFReport = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T14:30:25Z', - reporter: { - org: 'Security Corp', - contact: 'abuse@security.com', - domain: 'security.com', - }, - sender: { - org: 'Security Corp', - contact: 'abuse@security.com', - domain: 'security.com', - }, - source_identifier: '192.0.2.100', - category: 'content', - type: 'phishing', - evidence_source: 'automated_scan', - url: 'http://phishing.example.com', - }; - - const handCodedResult = handCodedValidator.validate(report); - expect(handCodedResult.valid).toBe(true); - - const schemaResult = schemaValidator.validateCore(report); - expect(schemaResult.valid).toBe(true); + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.field.includes('reporter'))).toBe(true); }); - }); - describe('6. Reports that violate schema but not hand-coded rules', () => { - it('should catch report with legacy_version other than "3"', () => { - const report = createValidReport(); + it('should reject legacy_version other than "3"', () => { + const report = getReport('connection/ddos'); (report as any).legacy_version = '2'; - const schemaResult = schemaValidator.validateCore(report); - const handCodedResult = handCodedValidator.validate(report); + const result = validator.validate(report); - // Schema enforces legacy_version must be "3" if present - expect(schemaResult.valid).toBe(false); - expect(schemaResult.errors.some((e: string) => e.includes('legacy_version'))).toBe(true); - // Hand-coded doesn't validate legacy_version - expect(handCodedResult.valid).toBe(true); + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.field === 'legacy_version')).toBe(true); }); - it('should catch evidence item missing required payload field', () => { - const report = createValidReport(); + it('should reject evidence item missing required payload field', () => { + const report = getReport('connection/ddos'); report.evidence = [ { content_type: 'text/plain', @@ -440,99 +258,175 @@ describe('JSON Schema Validation', () => { } as any, ]; - const schemaResult = schemaValidator.validateCore(report); - const handCodedResult = handCodedValidator.validate(report); + const result = validator.validate(report); - // Schema enforces required payload field - expect(schemaResult.valid).toBe(false); - expect(schemaResult.errors.some((e: string) => e.includes('payload'))).toBe(true); - // Hand-coded doesn't validate evidence structure deeply - expect(handCodedResult.valid).toBe(true); + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.message.includes('payload'))).toBe(true); }); }); - describe('7. Compare schema vs hand-coded validator results', () => { - it('should both pass for valid report', () => { - const report = createValidReport(); + describe('strict mode (x-recommended promotion)', () => { + it('should pass in normal mode when recommended fields are missing', () => { + const report = getReport('messaging/spam'); + delete (report as any).confidence; + delete (report as any).evidence; - const schemaResult = schemaValidator.validateCore(report); - const handCodedResult = handCodedValidator.validate(report); + const result = schemaValidator.validate(report, false); - expect(schemaResult.valid).toBe(true); - expect(handCodedResult.valid).toBe(true); - expect(handCodedResult.errors.length).toBe(0); + expect(result.valid).toBe(true); }); - it('should both fail for report missing required field', () => { - const report = createValidReport(); - delete (report as any).timestamp; + it('should fail in strict mode when core recommended fields are missing', () => { + const report = getReport('connection/ddos'); + delete (report as any).confidence; - const schemaResult = schemaValidator.validateCore(report); - const handCodedResult = handCodedValidator.validate(report); + const result = schemaValidator.validate(report, true); - // Both should fail - expect(schemaResult.valid).toBe(false); - expect(handCodedResult.valid).toBe(false); + expect(result.valid).toBe(false); + expect(result.errors.some((e: string) => e.includes('confidence'))).toBe(true); }); - it('should both fail for invalid email format', () => { - const report = createValidReport(); - report.reporter.contact = 'invalid-email'; + it('should pass in strict mode when all recommended fields are present', () => { + const report: XARFReport = { + ...getReport('connection/ddos'), + evidence_source: 'honeypot', + confidence: 0.95, + evidence: [ + { + content_type: 'text/plain', + payload: 'dGVzdA==', + description: 'Test evidence', + hash: 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', + }, + ], + }; - const schemaResult = schemaValidator.validateCore(report); - const handCodedResult = handCodedValidator.validate(report); + const result = schemaValidator.validate(report, true); + const coreRecommendedErrors = result.errors.filter( + (e: string) => + e.includes('confidence') || e.includes('source_port') || e.includes("'evidence'") + ); + expect(coreRecommendedErrors).toHaveLength(0); + }); + + it('should fail when nested evidence recommended fields are missing', () => { + const report = { + ...getReport('connection/ddos'), + confidence: 0.95, + evidence: [ + { + content_type: 'text/plain', + payload: 'dGVzdA==', + // Missing recommended: description, hash + }, + ], + } as unknown as XARFReport; + + const result = schemaValidator.validate(report, true); - // Both should fail - expect(schemaResult.valid).toBe(false); - expect(handCodedResult.valid).toBe(false); - expect(handCodedResult.errors.some((e) => e.field === 'reporter.contact')).toBe(true); + expect(result.valid).toBe(false); + const evidenceErrors = result.errors.filter( + (e: string) => e.includes('description') || e.includes('hash') + ); + expect(evidenceErrors.length).toBeGreaterThan(0); }); - it('should both fail for invalid confidence value', () => { - const report = createValidReport(); - report.confidence = 2.5; + it('should fail when type-specific recommended fields are missing', () => { + const report = getReport('messaging/spam'); + report.confidence = 0.9; + report.evidence = [ + { + content_type: 'text/plain', + payload: 'dGVzdA==', + description: 'spam email', + hash: 'sha256:abc123', + }, + ]; + // Missing type-specific recommended: smtp_to, subject, message_id - const schemaResult = schemaValidator.validateCore(report); - const handCodedResult = handCodedValidator.validate(report); + const result = schemaValidator.validate(report, true); - // Both should fail - expect(schemaResult.valid).toBe(false); - expect(handCodedResult.valid).toBe(false); - expect(handCodedResult.errors.some((e) => e.field === 'confidence')).toBe(true); + expect(result.valid).toBe(false); + expect( + result.errors.some( + (e: string) => e.includes('smtp_to') || e.includes('subject') || e.includes('message_id') + ) + ).toBe(true); }); }); - describe('Performance', () => { - it('should validate 100 reports quickly', () => { - const reports = Array(100) - .fill(null) - .map(() => createValidReport()); + describe('transformSchemaForStrict', () => { + it('should promote x-recommended properties to required', () => { + const schema = { + type: 'object', + required: ['name'], + properties: { + name: { type: 'string' }, + email: { type: 'string', 'x-recommended': true }, + }, + }; - const startTime = performance.now(); - reports.forEach((report) => { - schemaValidator.validateCore(report); - }); - const duration = performance.now() - startTime; + const transformed = schemaValidator.transformSchemaForStrict(schema) as any; + + expect(transformed.required).toContain('name'); + expect(transformed.required).toContain('email'); + }); + + it('should handle nested objects in $defs', () => { + const schema = { + type: 'object', + properties: {}, + $defs: { + nested: { + type: 'object', + required: ['id'], + properties: { + id: { type: 'string' }, + label: { type: 'string', 'x-recommended': true }, + }, + }, + }, + }; - expect(duration).toBeLessThan(1000); // Should be fast even with 100 reports + const transformed = schemaValidator.transformSchemaForStrict(schema) as any; + + expect(transformed.$defs.nested.required).toContain('id'); + expect(transformed.$defs.nested.required).toContain('label'); }); - it('should handle validation errors efficiently', () => { - const invalidReports = Array(100) - .fill(null) - .map(() => { - const report = createValidReport(); - delete (report as any).report_id; - return report; - }); - - const startTime = performance.now(); - invalidReports.forEach((report) => { - schemaValidator.validateCore(report); - }); - const duration = performance.now() - startTime; - - expect(duration).toBeLessThan(1500); // Should still be reasonably fast + it('should handle allOf composition', () => { + const schema = { + allOf: [ + { $ref: '../xarf-core.json' }, + { + required: ['protocol'], + properties: { + protocol: { type: 'string' }, + smtp_to: { type: 'string', 'x-recommended': true }, + }, + }, + ], + }; + + const transformed = schemaValidator.transformSchemaForStrict(schema) as any; + + expect(transformed.allOf[1].required).toContain('protocol'); + expect(transformed.allOf[1].required).toContain('smtp_to'); + }); + + it('should not mutate the original schema', () => { + const schema = { + type: 'object', + required: ['name'], + properties: { + name: { type: 'string' }, + email: { type: 'string', 'x-recommended': true }, + }, + }; + + schemaValidator.transformSchemaForStrict(schema); + + expect(schema.required).toEqual(['name']); }); }); }); diff --git a/tests/senior-engineer-feedback.test.ts b/tests/senior-engineer-feedback.test.ts deleted file mode 100644 index dd125d9..0000000 --- a/tests/senior-engineer-feedback.test.ts +++ /dev/null @@ -1,300 +0,0 @@ -/** - * Tests for senior engineer feedback issues - * - * Issues reported: - * 1. Snake case - XARF spec uses snake_case throughout - * 2. Invalid properties should emit warnings (silent failure is bad) - * 3. ReportType should alias to type - * 4. Timestamps should validate ISO format and throw if invalid - * 5. Generator creating invalid reports that fail validation - */ - -import { XARFParser } from '../src/parser'; -import { XARFGenerator } from '../src/generator'; -import { XARFValidator } from '../src/validator'; - -describe('Senior Engineer Feedback Issues', () => { - describe('Issue 1: Snake case vs camel case support', () => { - it('should accept snake_case properties from XARF spec examples', () => { - const parser = new XARFParser(); - - // This is how it appears in XARF v4 spec examples - const report = { - xarf_version: '4.0.0', - report_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Security Team', - contact: 'abuse@example.com', - domain: 'example.com', - }, - sender: { - org: 'Security Team', - contact: 'abuse@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.100', - category: 'content', - type: 'phishing', - evidence_source: 'honeypot', - url: 'http://phishing.example.com', - }; - - // This should NOT throw or have errors - const parsed = parser.parse(report); - expect(parsed.category).toBe('content'); - expect(parser.getErrors()).toHaveLength(0); - }); - - it('should work with XARF spec example directly copied', () => { - const parser = new XARFParser(); - - // Direct copy from spec would use snake_case throughout - const specExample = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2025-12-16T07:37:30.000Z', - reporter: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.100', - source_port: 12345, - category: 'connection', - type: 'ddos', - evidence_source: 'honeypot', - destination_ip: '203.0.113.10', - protocol: 'tcp', - first_seen: '2025-12-16T07:00:00.000Z', - }; - - const parsed = parser.parse(specExample); - expect(parsed).toBeDefined(); - expect(parser.getErrors()).toHaveLength(0); - }); - }); - - describe('Issue 2: Invalid properties should emit warnings', () => { - it('should warn when using incorrect property names', () => { - const parser = new XARFParser(); - - const reportWithTypos = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2025-12-16T07:37:30.000Z', - reporter: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.100', - category: 'content', - type: 'phishing', - evidence_source: 'honeypot', - url: 'http://phishing.example.com', - // Typo - should be 'severity' not 'severety' - severety: 'high', - // Another common mistake - camelCase instead of snake_case - sourcePort: 443, - }; - - parser.parse(reportWithTypos); - const warnings = parser.getWarnings(); - - // Should have warnings about unknown properties - expect(warnings.length).toBeGreaterThan(0); - expect(warnings.some((w) => w.includes('severety') || w.includes('unknown'))).toBe(true); - }); - - it('should warn about misspelled category-specific fields', () => { - const parser = new XARFParser(); - - const report = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2025-12-16T07:37:30.000Z', - reporter: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.100', - category: 'content', - type: 'phishing', - evidence_source: 'honeypot', - url: 'http://phishing.example.com', - // Typo: should be 'content_type' not 'contentType' - contentType: 'text/html', - }; - - parser.parse(report); - const warnings = parser.getWarnings(); - - // Should warn about contentType being unrecognized - expect(warnings.some((w) => w.includes('contentType') || w.includes('unknown'))).toBe(true); - }); - }); - - describe('Issue 3: ReportType should alias to type', () => { - it('should accept ReportType as alias for type field', () => { - const parser = new XARFParser(); - - const report = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2025-12-16T07:37:30.000Z', - reporter: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.100', - category: 'connection', - ReportType: 'ddos', // Using ReportType instead of type - evidence_source: 'honeypot', - destination_ip: '203.0.113.10', - protocol: 'tcp', - }; - - // Should either work or provide clear error - const parsed = parser.parse(report); - expect(parsed.type || (parsed as any).ReportType).toBe('ddos'); - }); - }); - - describe('Issue 4: Timestamp validation should enforce ISO format', () => { - it('should throw error for invalid timestamp format', () => { - const validator = new XARFValidator(); - - const report = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: 'foo', // Invalid timestamp - reporter: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.100', - category: 'content', - type: 'phishing', - evidence_source: 'honeypot', - url: 'http://phishing.example.com', - } as any; - - const result = validator.validate(report); - expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'timestamp')).toBe(true); - }); - }); - - describe('Issue 5: Generator should not create invalid reports', () => { - it('should accept category-specific fields directly via union types', () => { - const generator = new XARFGenerator(); - - // Content report with url as direct field (not in additionalFields) - const report = generator.generateReport({ - category: 'content', - type: 'phishing', - source_identifier: '192.0.2.100', - reporter: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - url: 'http://phishing.example.com', // Direct field via union type - }); - - expect(report.url).toBe('http://phishing.example.com'); - }); - - it('should validate generated reports pass XARFValidator', () => { - const generator = new XARFGenerator(); - const validator = new XARFValidator(); - - // Generate a content report with direct url field - const report = generator.generateReport({ - category: 'content', - type: 'phishing', - source_identifier: '192.0.2.100', - reporter: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - url: 'http://phishing.example.com', - }); - - // The generated report should always be valid - const result = validator.validate(report); - expect(result.valid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - - it('should accept connection category fields directly via union types', () => { - const generator = new XARFGenerator(); - - // Connection report with fields as direct properties - const report = generator.generateReport({ - category: 'connection', - type: 'ddos', - source_identifier: '192.0.2.100', - reporter: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - destination_ip: '203.0.113.50', - protocol: 'tcp', - destination_port: 443, - first_seen: '2024-01-15T09:00:00Z', - source_port: 12345, - }); - - expect(report.destination_ip).toBe('203.0.113.50'); - expect(report.protocol).toBe('tcp'); - expect(report.destination_port).toBe(443); - }); - }); -}); diff --git a/tests/v3-legacy.test.ts b/tests/v3-legacy.test.ts index ce15985..4e97f74 100644 --- a/tests/v3-legacy.test.ts +++ b/tests/v3-legacy.test.ts @@ -4,7 +4,7 @@ * Tests for v3 to v4 conversion and backward compatibility */ -import { XARFParser } from '../src/parser'; +import { parse } from '../src/parser'; import { isXARFv3, convertV3toV4, getV3DeprecationWarning } from '../src/v3-legacy'; import type { XARFv3Report } from '../src/v3-legacy'; @@ -42,7 +42,7 @@ describe('XARFv3 Detection', () => { it('should not detect v4 report as v3', () => { const v4Report = { - xarf_version: '4.0.0', + xarf_version: '4.2.0', report_id: 'test-id', timestamp: '2024-01-15T10:00:00Z', reporter: { contact: 'test@example.com', type: 'manual' }, @@ -85,7 +85,7 @@ describe('XARFv3 Conversion', () => { const warnings: string[] = []; const v4Report = convertV3toV4(v3Report, warnings); - expect(v4Report.xarf_version).toBe('4.0.0'); + expect(v4Report.xarf_version).toBe('4.2.0'); expect(v4Report.category).toBe('messaging'); expect(v4Report.type).toBe('spam'); expect(v4Report.source_identifier).toBe('192.168.1.100'); @@ -97,7 +97,7 @@ describe('XARFv3 Conversion', () => { expect(v4Report.sender.domain).toBe('antispam.example'); expect(v4Report.timestamp).toBe('2024-01-15T14:30:25Z'); expect(v4Report.description).toBe('Spam email detected'); - expect(v4Report._internal?.legacy_version).toBe('3'); + expect(v4Report.legacy_version).toBe('3'); expect(v4Report._internal?.original_report_type).toBe('Spam'); // Category-specific fields @@ -115,6 +115,7 @@ describe('XARFv3 Conversion', () => { Report: { ReportType: 'spam', Date: '2024-01-15T10:00:00Z', + Protocol: 'smtp', Source: { IP: '10.0.0.1', Port: 25, @@ -155,7 +156,30 @@ describe('XARFv3 Conversion', () => { expect((v4Report as any).destination_ip).toBe('198.51.100.10'); expect((v4Report as any).destination_port).toBe(80); expect((v4Report as any).protocol).toBe('tcp'); - expect((v4Report as any).attempt_count).toBe(10000); + expect((v4Report as any).attack_count).toBe(10000); + }); + + it('should not inject undefined fields for absent optional v3 fields', () => { + const v3Report: XARFv3Report = { + Version: '3', + ReporterInfo: { + ReporterOrgEmail: 'security@example.com', + }, + Report: { + ReportType: 'DDoS', + Date: '2024-01-15T15:00:00Z', + SourceIp: '203.0.113.50', + Protocol: 'tcp', + // No DestinationIp, DestinationPort, or AttackCount + }, + }; + + const v4Report = convertV3toV4(v3Report); + expect(v4Report.category).toBe('connection'); + expect(v4Report.type).toBe('ddos'); + expect('destination_ip' in v4Report).toBe(false); + expect('destination_port' in v4Report).toBe(false); + expect('attack_count' in v4Report).toBe(false); }); it('should convert v3 Login-Attack report', () => { @@ -170,6 +194,7 @@ describe('XARFv3 Conversion', () => { SourceIp: '192.0.2.50', DestinationIp: '203.0.113.10', DestinationPort: 22, + Protocol: 'tcp', }, }; @@ -188,6 +213,7 @@ describe('XARFv3 Conversion', () => { ReportType: 'Port-Scan', Date: '2024-01-15T12:00:00Z', SourceIp: '192.0.2.99', + Protocol: 'tcp', }, }; @@ -287,6 +313,7 @@ describe('XARFv3 Conversion', () => { ReportType: 'Spam', Date: '2024-01-15T10:00:00Z', SourceIp: '192.0.2.1', + Protocol: 'smtp', Attachment: [ { ContentType: 'message/rfc822', @@ -303,6 +330,7 @@ describe('XARFv3 Conversion', () => { expect(v4Report.evidence?.[0].content_type).toBe('message/rfc822'); expect(v4Report.evidence?.[0].payload).toBe('base64encodeddata'); expect(v4Report.evidence?.[0].description).toBe('Original email'); + expect(v4Report.evidence?.[0].size).toBe(Buffer.from('base64encodeddata', 'base64').length); }); it('should convert v3 Samples to v4 evidence', () => { @@ -315,6 +343,7 @@ describe('XARFv3 Conversion', () => { ReportType: 'Malware', Date: '2024-01-15T10:00:00Z', SourceIp: '192.0.2.1', + Url: 'http://malware.example/payload', Samples: [ { ContentType: 'application/octet-stream', @@ -327,12 +356,12 @@ describe('XARFv3 Conversion', () => { const v4Report = convertV3toV4(v3Report); expect(v4Report.evidence).toBeDefined(); expect(v4Report.evidence?.[0].content_type).toBe('application/octet-stream'); - expect(v4Report.evidence?.[0].description).toBe('Evidence from v3 report'); + expect(v4Report.evidence?.[0].description).toBeUndefined(); }); }); describe('Unknown Type Handling', () => { - it('should handle unknown v3 report type with warning', () => { + it('should throw on unknown v3 report type', () => { const v3Report: XARFv3Report = { Version: '3', ReporterInfo: { @@ -345,41 +374,176 @@ describe('XARFv3 Conversion', () => { }, }; + expect(() => convertV3toV4(v3Report)).toThrow("unknown ReportType 'UnknownType'"); + }); + }); + + describe('Missing Reporter Email Handling', () => { + it('should throw when both reporter email fields are absent', () => { + const v3Report = { + Version: '3', + ReporterInfo: {}, + Report: { + ReportType: 'Spam', + Date: '2024-01-15T10:00:00Z', + SourceIp: '192.0.2.1', + }, + } as XARFv3Report; + + expect(() => convertV3toV4(v3Report)).toThrow('missing reporter email'); + }); + + it('should throw when reporter email has no domain part', () => { + const v3Report = { + Version: '3', + ReporterInfo: { + ReporterOrgEmail: 'not-an-email', + }, + Report: { + ReportType: 'Spam', + Date: '2024-01-15T10:00:00Z', + SourceIp: '192.0.2.1', + }, + } as XARFv3Report; + + expect(() => convertV3toV4(v3Report)).toThrow('not a valid email address'); + }); + + it('should warn when ReporterOrg is missing', () => { + const v3Report: XARFv3Report = { + Version: '3', + ReporterInfo: { + ReporterOrgEmail: 'test@example.com', + }, + Report: { + ReportType: 'Spam', + Date: '2024-01-15T10:00:00Z', + SourceIp: '192.0.2.1', + Protocol: 'smtp', + }, + }; + const warnings: string[] = []; const v4Report = convertV3toV4(v3Report, warnings); - expect(v4Report.category).toBe('content'); - expect(v4Report.type).toBe('unclassified'); - expect(warnings.length).toBeGreaterThan(0); - expect(warnings[0]).toContain('Unknown v3 ReportType'); + expect(warnings.some((w) => w.includes('No ReporterOrg found'))).toBe(true); + expect(v4Report.reporter.org).toBe('Unknown Organization'); + }); + }); + + describe('Missing Source Identifier Handling', () => { + it('should throw when no source identifier can be extracted', () => { + const v3Report: XARFv3Report = { + Version: '3', + ReporterInfo: { + ReporterOrg: 'Test Org', + ReporterOrgEmail: 'test@example.com', + }, + Report: { + ReportType: 'Botnet', + Date: '2024-01-15T10:00:00Z', + }, + }; + + expect(() => convertV3toV4(v3Report)).toThrow('no source identifier found'); + }); + + it('should extract source identifier from Source.URL when no IP is present', () => { + const v3Report: XARFv3Report = { + Version: '3', + ReporterInfo: { + ReporterOrg: 'Security Vendor', + ReporterOrgEmail: 'abuse@security.example', + }, + Report: { + ReportType: 'Phishing', + Date: '2024-01-15T10:00:00Z', + Source: { URL: 'https://malicious-example.net/banking-login/' }, + Url: 'https://malicious-example.net/banking-login/', + }, + }; + + const v4Report = convertV3toV4(v3Report); + expect(v4Report.source_identifier).toBe('https://malicious-example.net/banking-login/'); + expect((v4Report as any).url).toBe('https://malicious-example.net/banking-login/'); + }); + + it('should extract source identifier from Url when no Source is present', () => { + const v3Report: XARFv3Report = { + Version: '3', + ReporterInfo: { + ReporterOrg: 'Test Org', + ReporterOrgEmail: 'test@example.com', + }, + Report: { + ReportType: 'Malware', + Date: '2024-01-15T10:00:00Z', + Url: 'http://malware.example/payload.exe', + }, + }; + + const v4Report = convertV3toV4(v3Report); + expect(v4Report.source_identifier).toBe('http://malware.example/payload.exe'); }); }); - describe('Missing Source IP Handling', () => { - it('should handle missing source IP with warning', () => { + describe('Missing Protocol Handling', () => { + it('should throw when messaging report has no protocol', () => { const v3Report: XARFv3Report = { Version: '3', ReporterInfo: { + ReporterOrg: 'Test Org', ReporterOrgEmail: 'test@example.com', }, Report: { ReportType: 'Spam', Date: '2024-01-15T10:00:00Z', + SourceIp: '192.0.2.1', }, }; - const warnings: string[] = []; - const v4Report = convertV3toV4(v3Report, warnings); + expect(() => convertV3toV4(v3Report)).toThrow('missing protocol for messaging type'); + }); + + it('should throw when connection report has no protocol', () => { + const v3Report: XARFv3Report = { + Version: '3', + ReporterInfo: { + ReporterOrg: 'Test Org', + ReporterOrgEmail: 'test@example.com', + }, + Report: { + ReportType: 'DDoS', + Date: '2024-01-15T10:00:00Z', + SourceIp: '192.0.2.1', + }, + }; + + expect(() => convertV3toV4(v3Report)).toThrow('missing protocol for connection type'); + }); + }); - expect(v4Report.source_identifier).toBe('unknown'); - expect(warnings.some((w) => w.includes('No source IP found'))).toBe(true); + describe('Missing URL Handling', () => { + it('should throw when content report has no URL', () => { + const v3Report: XARFv3Report = { + Version: '3', + ReporterInfo: { + ReporterOrg: 'Test Org', + ReporterOrgEmail: 'test@example.com', + }, + Report: { + ReportType: 'Phishing', + Date: '2024-01-15T10:00:00Z', + SourceIp: '192.0.2.100', + }, + }; + + expect(() => convertV3toV4(v3Report)).toThrow('missing URL for content type'); }); }); }); describe('XARFParser v3 Integration', () => { - let parser: XARFParser; - beforeEach(() => { // Mock console.warn to avoid noise in tests jest.spyOn(console, 'warn').mockImplementation(() => {}); @@ -390,8 +554,6 @@ describe('XARFParser v3 Integration', () => { }); it('should parse v3 spam report automatically', () => { - parser = new XARFParser(false); - const v3Report = { Version: '3', ReporterInfo: { @@ -408,21 +570,18 @@ describe('XARFParser v3 Integration', () => { }, }; - const result = parser.parse(v3Report); + const { report, warnings } = parse(v3Report); - expect(result.xarf_version).toBe('4.0.0'); - expect(result.category).toBe('messaging'); - expect(result.type).toBe('spam'); - expect(result._internal?.legacy_version).toBe('3'); + expect(report.xarf_version).toBe('4.2.0'); + expect(report.category).toBe('messaging'); + expect(report.type).toBe('spam'); + expect(report.legacy_version).toBe('3'); - const warnings = parser.getWarnings(); expect(warnings.length).toBeGreaterThan(0); expect(warnings[0]).toContain('DEPRECATION WARNING'); }); it('should validate v3 report as valid', () => { - parser = new XARFParser(); - const v3Report = { Version: '3', ReporterInfo: { @@ -432,21 +591,19 @@ describe('XARFParser v3 Integration', () => { ReportType: 'DDoS', Date: '2024-01-15T10:00:00Z', SourceIp: '192.0.2.50', + SourcePort: 54321, DestinationIp: '203.0.113.10', Protocol: 'tcp', }, }; - const isValid = parser.validate(v3Report); - expect(isValid).toBe(true); + const { errors, warnings } = parse(v3Report); + expect(errors).toHaveLength(0); - const warnings = parser.getWarnings(); expect(warnings.length).toBeGreaterThan(0); }); it('should provide warnings when parsing v3 report', () => { - parser = new XARFParser(); - const v3Report = { Version: '3', ReporterInfo: { @@ -456,14 +613,14 @@ describe('XARFParser v3 Integration', () => { ReportType: 'Spam', Date: '2024-01-15T10:00:00Z', SourceIp: '192.0.2.1', + Protocol: 'smtp', }, }; - parser.parse(v3Report); - const warnings = parser.getWarnings(); + const { warnings } = parse(v3Report); expect(warnings.length).toBeGreaterThan(0); - expect(warnings.some((w) => w.includes('v3 format'))).toBe(true); + expect(warnings.some((w: string) => w.includes('v3 format'))).toBe(true); }); }); diff --git a/tests/validator.edge-cases.test.ts b/tests/validator.edge-cases.test.ts deleted file mode 100644 index db6fbea..0000000 --- a/tests/validator.edge-cases.test.ts +++ /dev/null @@ -1,334 +0,0 @@ -/** - * Edge Case Tests for XARF Validator - */ - -import { XARFValidator } from '../src/validator'; -import type { XARFReport } from '../src/types'; - -describe('XARFValidator Edge Cases', () => { - let validator: XARFValidator; - - beforeEach(() => { - validator = new XARFValidator(); - }); - - describe('validateRequiredFields edge cases', () => { - it('should detect missing reporter.contact', () => { - const report = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test Org', - type: 'automated', - }, - source_identifier: '192.0.2.1', - category: 'messaging', - type: 'spam', - evidence_source: 'spamtrap', - } as unknown as XARFReport; - - const result = validator.validate(report); - - expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'reporter.contact')).toBe(true); - }); - - it('should detect missing reporter.domain', () => { - const report = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test Org', - contact: 'test@example.com', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'messaging', - type: 'spam', - evidence_source: 'spamtrap', - } as unknown as XARFReport; - - const result = validator.validate(report); - - expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'reporter.domain')).toBe(true); - }); - }); - - describe('validateFormats edge cases', () => { - it('should handle invalid timestamp that causes exception', () => { - const report = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'messaging', - type: 'spam', - evidence_source: 'spamtrap', - protocol: 'chat', // Required for messaging/spam, use chat to avoid smtp requirements - } as unknown as XARFReport; - - const result = validator.validate(report); - - // Valid timestamp should pass - expect(result.valid).toBe(true); - }); - }); - - describe('validateValues edge cases', () => { - it('should validate invalid evidence_source', () => { - const report = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'messaging', - type: 'spam', - evidence_source: 'invalid_source', - } as unknown as XARFReport; - - const result = validator.validate(report); - - expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'evidence_source')).toBe(true); - }); - }); - - describe('validateCategorySpecific edge cases', () => { - it('should error for invalid connection type', () => { - const report = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'connection', - type: 'invalid_connection_type', - evidence_source: 'ids_ips', - destination_ip: '203.0.113.1', - protocol: 'tcp', - } as XARFReport; - - const result = validator.validate(report); - - expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'type')).toBe(true); - expect(result.errors.some((e) => e.message.includes('Invalid type'))).toBe(true); - }); - - it('should error for invalid content type', () => { - const report = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'content', - type: 'invalid_content_type', - evidence_source: 'user_report', - url: 'http://example.com', - } as XARFReport; - - const result = validator.validate(report); - - expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'type')).toBe(true); - expect(result.errors.some((e) => e.message.includes('Invalid type'))).toBe(true); - }); - - it('should handle infrastructure category with no specific validation', () => { - const report = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'infrastructure', - type: 'botnet', - evidence_source: 'honeypot', - compromise_evidence: 'C2 communication observed', // Required for botnet type - } as XARFReport; - - const result = validator.validate(report); - - // Should validate without category-specific errors - expect(result.valid).toBe(true); - }); - }); - - describe('validateConnectionReport edge cases', () => { - it('should validate invalid port number (non-integer)', () => { - const report = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'connection', - type: 'ddos', - evidence_source: 'honeypot', - destination_ip: '203.0.113.1', - protocol: 'tcp', - destination_port: 'not-a-number', - } as unknown as XARFReport; - - const result = validator.validate(report); - - expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'destination_port')).toBe(true); - }); - - it('should validate port number too high', () => { - const report = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'connection', - type: 'ddos', - evidence_source: 'honeypot', - destination_ip: '203.0.113.1', - protocol: 'tcp', - destination_port: 70000, - } as XARFReport; - - const result = validator.validate(report); - - expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'destination_port')).toBe(true); - }); - - it('should validate negative port number', () => { - const report = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'connection', - type: 'ddos', - evidence_source: 'honeypot', - destination_ip: '203.0.113.1', - protocol: 'tcp', - destination_port: -1, - } as XARFReport; - - const result = validator.validate(report); - - expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'destination_port')).toBe(true); - }); - }); - - describe('validateContentReport edge cases', () => { - it('should catch URL parsing error', () => { - const report = { - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T10:30:00Z', - reporter: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - sender: { - org: 'Test Org', - contact: 'test@example.com', - domain: 'example.com', - }, - source_identifier: '192.0.2.1', - category: 'content', - type: 'phishing', - evidence_source: 'user_report', - url: 'not-a-valid-url', - } as XARFReport; - - const result = validator.validate(report); - - expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'url')).toBe(true); - expect(result.errors.some((e) => e.message.includes('Invalid URL format'))).toBe(true); - }); - }); -}); diff --git a/tests/validator.test.ts b/tests/validator.test.ts index 8363ce1..f27b331 100644 --- a/tests/validator.test.ts +++ b/tests/validator.test.ts @@ -3,7 +3,6 @@ */ import { XARFValidator } from '../src/validator'; -import { XARFValidationError } from '../src/errors'; import type { XARFReport } from '../src/types'; describe('XARFValidator', () => { @@ -14,7 +13,7 @@ describe('XARFValidator', () => { }); const createValidReport = (): XARFReport => ({ - xarf_version: '4.0.0', + xarf_version: '4.2.0', report_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', timestamp: '2024-01-15T10:30:00Z', reporter: { @@ -38,17 +37,9 @@ describe('XARFValidator', () => { }); describe('validate', () => { - it('should validate correct report', () => { - const report = createValidReport(); - const result = validator.validate(report); - - expect(result.valid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - it('should detect missing required fields', () => { const report: any = { - xarf_version: '4.0.0', + xarf_version: '4.2.0', // Missing other required fields }; @@ -58,99 +49,206 @@ describe('XARFValidator', () => { expect(result.errors.length).toBeGreaterThan(0); }); - it('should detect invalid XARF version', () => { + it('should detect invalid category', () => { const report = createValidReport(); - report.xarf_version = '3.0.0'; + (report as any).category = 'invalid'; const result = validator.validate(report); expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'xarf_version')).toBe(true); + expect(result.errors.some((e) => e.field === 'category')).toBe(true); }); - it('should detect invalid category', () => { + it('should return invalid result in strict mode', () => { const report = createValidReport(); - (report as any).category = 'invalid'; - - const result = validator.validate(report); + report.xarf_version = '3.0.0'; + const result = validator.validate(report, true); expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'category')).toBe(true); + expect(result.errors.length).toBeGreaterThan(0); + }); + + it('should convert warnings to errors in strict mode', () => { + const report = createValidReport(); + (report as any).unknownField = 'some value'; + + // Non-strict: valid with warning + const nonStrictResult = validator.validate(report, false); + expect(nonStrictResult.valid).toBe(true); + expect(nonStrictResult.warnings.some((w) => w.field === 'unknownField')).toBe(true); + + // Strict: warning becomes error + const strictResult = validator.validate(report, true); + expect(strictResult.valid).toBe(false); + expect(strictResult.errors.some((e) => e.field === 'unknownField')).toBe(true); }); + }); - it('should detect invalid reporter domain', () => { + describe('format validation', () => { + it('should error on invalid UUID format', () => { const report = createValidReport(); - (report.reporter as any).domain = 'invalid domain with spaces'; + report.report_id = 'not-a-valid-uuid'; const result = validator.validate(report); expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'reporter.domain')).toBe(true); + expect(result.errors.some((e) => e.field === 'report_id')).toBe(true); }); - it('should detect invalid confidence', () => { + it('should detect invalid timestamp', () => { const report = createValidReport(); - report.confidence = 1.5; + report.timestamp = 'invalid-timestamp'; const result = validator.validate(report); expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'confidence')).toBe(true); + expect(result.errors.some((e) => e.field === 'timestamp')).toBe(true); }); - it('should throw in strict mode', () => { + it('should detect invalid version format', () => { const report = createValidReport(); - report.xarf_version = '3.0.0'; + report.xarf_version = '4.0'; - expect(() => validator.validate(report, true)).toThrow(XARFValidationError); + const result = validator.validate(report); + + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.field === 'xarf_version')).toBe(true); }); - it('should convert warnings to errors in strict mode', () => { - const report = createValidReport(); - report.report_id = 'not-a-uuid'; + it('should reject invalid timestamp string', () => { + const report = { + xarf_version: '4.2.0', + report_id: '550e8400-e29b-41d4-a716-446655440000', + timestamp: 'foo', + reporter: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + sender: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + source_identifier: '192.0.2.100', + category: 'content', + type: 'phishing', + evidence_source: 'honeypot', + url: 'http://phishing.example.com', + } as any; - expect(() => validator.validate(report, true)).toThrow(XARFValidationError); + const result = validator.validate(report); + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.field === 'timestamp')).toBe(true); }); - }); - describe('format validation', () => { - it('should warn about invalid UUID format', () => { - const report = createValidReport(); - report.report_id = 'not-a-valid-uuid'; + it('should pass with valid timestamp', () => { + const report = { + xarf_version: '4.2.0', + report_id: '550e8400-e29b-41d4-a716-446655440000', + timestamp: '2024-01-15T10:30:00Z', + reporter: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + sender: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + source_identifier: '192.0.2.1', + category: 'messaging', + type: 'spam', + evidence_source: 'spamtrap', + protocol: 'chat', + } as unknown as XARFReport; const result = validator.validate(report); - expect(result.warnings.some((w) => w.field === 'report_id')).toBe(true); + expect(result.valid).toBe(true); }); + }); - it('should error on invalid email format', () => { - const report = createValidReport(); - report.reporter.contact = 'not-an-email'; + describe('required fields edge cases', () => { + it('should detect missing reporter.contact', () => { + const report = { + xarf_version: '4.2.0', + report_id: '550e8400-e29b-41d4-a716-446655440000', + timestamp: '2024-01-15T10:30:00Z', + reporter: { + org: 'Test Org', + type: 'automated', + }, + source_identifier: '192.0.2.1', + category: 'messaging', + type: 'spam', + evidence_source: 'spamtrap', + } as unknown as XARFReport; const result = validator.validate(report); expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'reporter.contact')).toBe(true); - }); - - it('should detect invalid timestamp', () => { - const report = createValidReport(); - report.timestamp = 'invalid-timestamp'; + expect( + result.errors.some((e) => e.field.includes('reporter') && e.message.includes('contact')) + ).toBe(true); + }); + + it('should detect missing reporter.domain', () => { + const report = { + xarf_version: '4.2.0', + report_id: '550e8400-e29b-41d4-a716-446655440000', + timestamp: '2024-01-15T10:30:00Z', + reporter: { + org: 'Test Org', + contact: 'test@example.com', + }, + sender: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + source_identifier: '192.0.2.1', + category: 'messaging', + type: 'spam', + evidence_source: 'spamtrap', + } as unknown as XARFReport; const result = validator.validate(report); expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'timestamp')).toBe(true); + expect( + result.errors.some((e) => e.field.includes('reporter') && e.message.includes('domain')) + ).toBe(true); }); + }); - it('should detect invalid version format', () => { - const report = createValidReport(); - report.xarf_version = '4.0'; + describe('value validation', () => { + it('should validate invalid evidence_source', () => { + const report = { + xarf_version: '4.2.0', + report_id: '550e8400-e29b-41d4-a716-446655440000', + timestamp: '2024-01-15T10:30:00Z', + reporter: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + sender: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + source_identifier: '192.0.2.1', + category: 'messaging', + type: 'spam', + evidence_source: 'invalid_source', + } as unknown as XARFReport; const result = validator.validate(report); expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'xarf_version')).toBe(true); + expect(result.errors.some((e) => e.field === 'evidence_source')).toBe(true); }); }); @@ -162,13 +260,14 @@ describe('XARFValidator', () => { type: 'spam', protocol: 'smtp', smtp_from: 'spammer@example.com', + subject: 'Buy now!', }; const result = validator.validate(report); expect(result.valid).toBe(true); }); - it('should detect invalid messaging type', () => { + it('should reject unknown type', () => { const report: XARFReport = { ...createValidReport(), category: 'messaging', @@ -178,7 +277,6 @@ describe('XARFValidator', () => { const result = validator.validate(report); expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'type')).toBe(true); }); it('should require smtp_from for SMTP messaging', () => { @@ -192,7 +290,7 @@ describe('XARFValidator', () => { const result = validator.validate(report); expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'smtp_from')).toBe(true); + expect(result.errors.some((e) => e.message.includes('smtp_from'))).toBe(true); }); it('should validate connection reports', () => { @@ -202,24 +300,95 @@ describe('XARFValidator', () => { expect(result.valid).toBe(true); }); - it('should require destination_ip for connection reports', () => { + it('should accept missing destination_ip (recommended, not required)', () => { const report: any = createValidReport(); delete report.destination_ip; const result = validator.validate(report); + expect(result.valid).toBe(true); + }); + + it('should reject unknown connection type', () => { + const report = { + xarf_version: '4.2.0', + report_id: '550e8400-e29b-41d4-a716-446655440000', + timestamp: '2024-01-15T10:30:00Z', + reporter: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + sender: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + source_identifier: '192.0.2.1', + category: 'connection', + type: 'invalid_connection_type', + evidence_source: 'ids_ips', + destination_ip: '203.0.113.1', + protocol: 'tcp', + } as XARFReport; + + const result = validator.validate(report); + expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'destination_ip')).toBe(true); }); - it('should validate port numbers', () => { - const report = createValidReport(); - (report as any).destination_port = 70000; + it('should reject unknown content type', () => { + const report = { + xarf_version: '4.2.0', + report_id: '550e8400-e29b-41d4-a716-446655440000', + timestamp: '2024-01-15T10:30:00Z', + reporter: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + sender: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + source_identifier: '192.0.2.1', + category: 'content', + type: 'invalid_content_type', + evidence_source: 'user_report', + url: 'http://example.com', + } as XARFReport; const result = validator.validate(report); expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'destination_port')).toBe(true); + }); + + it('should handle infrastructure category', () => { + const report = { + xarf_version: '4.2.0', + report_id: '550e8400-e29b-41d4-a716-446655440000', + timestamp: '2024-01-15T10:30:00Z', + reporter: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + sender: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + source_identifier: '192.0.2.1', + category: 'infrastructure', + type: 'botnet', + evidence_source: 'honeypot', + compromise_evidence: 'C2 communication observed', + } as XARFReport; + + const result = validator.validate(report); + + expect(result.valid).toBe(true); }); it('should validate content reports', () => { @@ -248,7 +417,9 @@ describe('XARFValidator', () => { const result = validator.validate(report); expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.field === 'url')).toBe(true); + expect( + result.errors.some((e) => e.message.includes('url') && e.message.includes('required')) + ).toBe(true); }); it('should validate URL format', () => { @@ -263,8 +434,131 @@ describe('XARFValidator', () => { const result = validator.validate(report); + expect(result.valid).toBe(false); + expect( + result.errors.some((e) => e.message.includes('url') || e.message.includes('format')) + ).toBe(true); + }); + + it('should catch URL parsing error with field check', () => { + const report = { + xarf_version: '4.2.0', + report_id: '550e8400-e29b-41d4-a716-446655440000', + timestamp: '2024-01-15T10:30:00Z', + reporter: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + sender: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + source_identifier: '192.0.2.1', + category: 'content', + type: 'phishing', + evidence_source: 'user_report', + url: 'not-a-valid-url', + } as XARFReport; + + const result = validator.validate(report); + expect(result.valid).toBe(false); expect(result.errors.some((e) => e.field === 'url')).toBe(true); + expect(result.errors.some((e) => e.message.includes('format'))).toBe(true); + }); + }); + + describe('connection port validation', () => { + it('should validate invalid port number (non-integer)', () => { + const report = { + xarf_version: '4.2.0', + report_id: '550e8400-e29b-41d4-a716-446655440000', + timestamp: '2024-01-15T10:30:00Z', + reporter: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + sender: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + source_identifier: '192.0.2.1', + category: 'connection', + type: 'ddos', + evidence_source: 'honeypot', + destination_ip: '203.0.113.1', + protocol: 'tcp', + destination_port: 'not-a-number', + } as unknown as XARFReport; + + const result = validator.validate(report); + + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.field === 'destination_port')).toBe(true); + }); + + it('should validate port number too high', () => { + const report = { + xarf_version: '4.2.0', + report_id: '550e8400-e29b-41d4-a716-446655440000', + timestamp: '2024-01-15T10:30:00Z', + reporter: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + sender: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + source_identifier: '192.0.2.1', + category: 'connection', + type: 'ddos', + evidence_source: 'honeypot', + destination_ip: '203.0.113.1', + protocol: 'tcp', + destination_port: 70000, + } as XARFReport; + + const result = validator.validate(report); + + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.field === 'destination_port')).toBe(true); + }); + + it('should validate negative port number', () => { + const report = { + xarf_version: '4.2.0', + report_id: '550e8400-e29b-41d4-a716-446655440000', + timestamp: '2024-01-15T10:30:00Z', + reporter: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + sender: { + org: 'Test Org', + contact: 'test@example.com', + domain: 'example.com', + }, + source_identifier: '192.0.2.1', + category: 'connection', + type: 'ddos', + evidence_source: 'honeypot', + destination_ip: '203.0.113.1', + protocol: 'tcp', + destination_port: -1, + } as XARFReport; + + const result = validator.validate(report); + + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.field === 'destination_port')).toBe(true); }); }); @@ -325,7 +619,6 @@ describe('XARFValidator', () => { const result = validator.validate(report, false, true); expect(result.info).toBeDefined(); - // Should include common optional fields like description, confidence, tags, etc. const infoFields = result.info!.map((i) => i.field); expect(infoFields).toContain('description'); expect(infoFields).toContain('confidence'); @@ -334,13 +627,11 @@ describe('XARFValidator', () => { it('should include type-specific optional fields', () => { const report = createValidReport(); - // This is a connection/ddos report const result = validator.validate(report, false, true); expect(result.info).toBeDefined(); const infoFields = result.info!.map((i) => i.field); - // Connection DDoS specific optional fields expect(infoFields).toContain('destination_port'); }); @@ -368,6 +659,34 @@ describe('XARFValidator', () => { expect(descriptionInfo!.message).toContain('OPTIONAL'); }); + it('should include optional fields from content-base.json via $ref', () => { + const report = { + xarf_version: '4.2.0', + report_id: '550e8400-e29b-41d4-a716-446655440000', + timestamp: '2024-01-15T10:30:00Z', + reporter: { org: 'Test', contact: 'test@example.com', domain: 'example.com' }, + sender: { org: 'Test', contact: 'test@example.com', domain: 'example.com' }, + source_identifier: '192.0.2.1', + category: 'content', + type: 'phishing', + url: 'https://phishing.example.com/login', + confidence: 0.95, + evidence: [{ content_type: 'text/plain', payload: 'dGVzdA==', description: 'test' }], + verified_at: '2024-01-15T10:30:00Z', + verification_method: 'manual', + target_brand: 'TestBrand', + domain: 'phishing.example.com', + } as XARFReport; + + const result = validator.validate(report, false, true); + + expect(result.info).toBeDefined(); + const infoFields = result.info!.map((i) => i.field); + expect(infoFields).toContain('registrar'); + expect(infoFields).toContain('hosting_provider'); + expect(infoFields).toContain('country_code'); + }); + it('should mark recommended fields appropriately', () => { const report = createValidReport(); @@ -388,7 +707,7 @@ describe('XARFValidator', () => { const result = validator.validate(report); - expect(result.valid).toBe(true); // Unknown fields are warnings, not errors + expect(result.valid).toBe(true); expect(result.warnings.length).toBeGreaterThanOrEqual(2); const unknownFieldWarnings = result.warnings.filter((w) => w.message.includes('Unknown field') @@ -427,7 +746,6 @@ describe('XARFValidator', () => { it('should not warn about known category-specific fields', () => { const report = createValidReport(); - // destination_port is a known connection category field report.destination_port = 443; const result = validator.validate(report); @@ -442,7 +760,9 @@ describe('XARFValidator', () => { const report = createValidReport(); (report as any).unknownField = 'some value'; - expect(() => validator.validate(report, true)).toThrow(); + const result = validator.validate(report, true); + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.field === 'unknownField')).toBe(true); }); }); }); diff --git a/validate-all-examples.js b/validate-all-examples.js deleted file mode 100644 index eab7d0a..0000000 --- a/validate-all-examples.js +++ /dev/null @@ -1,154 +0,0 @@ -const { XARFGenerator, XARFParser, XARFValidator, SchemaValidator } = require('./dist/index'); - -const validator = new XARFValidator(); -const schemaValidator = new SchemaValidator(); -const generator = new XARFGenerator(); -const parser = new XARFParser(); - -console.log('=== VALIDATING ALL EXAMPLES ===\n'); - -let totalExamples = 0; -let passedExamples = 0; -let failedExamples = 0; - -async function runTests() { - -async function validateExample(name, report) { - totalExamples++; - console.log(`Testing: ${name}`); - - // Test hand-coded validator - const handCodedResult = await validator.validate(report); - console.log(` Hand-coded validator: ${handCodedResult.valid ? '✅ PASS' : '❌ FAIL'}`); - if (!handCodedResult.valid && handCodedResult.errors && handCodedResult.errors.length > 0) { - console.log(` Errors:`, handCodedResult.errors.slice(0, 3).map(e => e.field + ': ' + e.message)); - } - - // Test schema validator (core) - const schemaResult = schemaValidator.validateCore(report); - console.log(` Schema validator: ${schemaResult.valid ? '✅ PASS' : '❌ FAIL'}`); - if (!schemaResult.valid && schemaResult.errors && schemaResult.errors.length > 0) { - console.log(` Errors:`, schemaResult.errors.slice(0, 3)); - } - - if (handCodedResult.valid && schemaResult.valid) { - passedExamples++; - console.log(' ✅ VALID\n'); - } else { - failedExamples++; - console.log(' ❌ INVALID\n'); - } -} - -// Example 1: README Quick Start - Generating -console.log('--- README Examples ---\n'); -const readmeExample1 = generator.generateReport({ - category: 'messaging', - type: 'spam', - source_identifier: '192.0.2.100', - reporter: { - org: 'Example Security', - contact: 'abuse@example.com', - domain: 'example.com' - }, - sender: { - org: 'Example Security', - contact: 'abuse@example.com', - domain: 'example.com' - }, - evidence_source: 'automated_scan', - severity: 'medium', - description: 'Spam email detected from source', - tags: ['spam:email'] -}); -await validateExample('README Quick Start - Generating', readmeExample1); - -// Example 2: README Quick Start - Parsing -const readmeExample2Json = JSON.stringify({ - xarf_version: '4.0.0', - report_id: '550e8400-e29b-41d4-a716-446655440000', - timestamp: '2024-01-15T14:30:25Z', - reporter: { - org: 'Example Security', - contact: 'abuse@example.com', - domain: 'example.com' - }, - sender: { - org: 'Example Security', - contact: 'abuse@example.com', - domain: 'example.com' - }, - source_identifier: '192.0.2.100', - category: 'messaging', - type: 'spam', - evidence_source: 'spamtrap', - protocol: 'smtp', - smtp_from: 'spammer@evil.com' -}); -const readmeExample2 = parser.parse(readmeExample2Json); -await validateExample('README Quick Start - Parsing', readmeExample2); - -// Example 3: Generator with all categories -console.log('--- Generator Sample Reports ---\n'); -const categoriesWithTypes = [ - { category: 'messaging', type: 'spam' }, - { category: 'connection', type: 'ddos' }, - { category: 'content', type: 'phishing' }, - { category: 'infrastructure', type: 'botnet' }, - { category: 'copyright', type: 'copyright' }, - { category: 'vulnerability', type: 'cve' }, - { category: 'reputation', type: 'blocklist' } -]; -for (const { category, type } of categoriesWithTypes) { - try { - const sample = generator.generateSampleReport(category, type, true, true); - await validateExample(`Generator Sample: ${category}/${type}`, sample); - } catch (error) { - totalExamples++; - failedExamples++; - console.log(`Generator Sample: ${category}/${type}`); - console.log(` ❌ FAILED TO GENERATE: ${error.message}\n`); - } -} - -// Example 4: Check examples in JSON schemas -console.log('--- JSON Schema Examples ---\n'); -const fs = require('fs'); -const path = require('path'); - -const typesDir = './src/schemas/types'; -const files = fs.readdirSync(typesDir).filter(f => f.endsWith('.json')); - -for (const file of files) { - try { - const schema = JSON.parse(fs.readFileSync(path.join(typesDir, file), 'utf-8')); - if (schema.examples && Array.isArray(schema.examples)) { - for (let i = 0; i < schema.examples.length; i++) { - await validateExample(`${file} - Example ${i+1}`, schema.examples[i]); - } - } - } catch (error) { - console.log(`Error reading ${file}: ${error.message}\n`); - } -} - -// Summary -console.log('=== SUMMARY ==='); -console.log(`Total examples tested: ${totalExamples}`); -console.log(`Passed: ${passedExamples} ✅`); -console.log(`Failed: ${failedExamples} ❌`); -console.log(`Success rate: ${((passedExamples/totalExamples)*100).toFixed(1)}%`); - -if (failedExamples > 0) { - console.log('\n⚠️ SOME EXAMPLES DO NOT VALIDATE!'); - process.exit(1); -} else { - console.log('\n✅ ALL EXAMPLES VALIDATE!'); - process.exit(0); -} -} - -runTests().catch(err => { - console.error('Fatal error:', err); - process.exit(1); -});