diff --git a/.claude-flow/metrics/performance.json b/.claude-flow/metrics/performance.json index 545ef53..9102870 100644 --- a/.claude-flow/metrics/performance.json +++ b/.claude-flow/metrics/performance.json @@ -1,7 +1,7 @@ { - "startTime": 1763985009551, - "sessionId": "session-1763985009551", - "lastActivity": 1763985009551, + "startTime": 1763991837598, + "sessionId": "session-1763991837598", + "lastActivity": 1763991837598, "sessionDuration": 0, "totalTasks": 1, "successfulTasks": 1, @@ -84,4 +84,4 @@ "cacheHits": 0, "cacheMisses": 0 } -} \ No newline at end of file +} diff --git a/.claude-flow/metrics/task-metrics.json b/.claude-flow/metrics/task-metrics.json index c18dc0c..6e35954 100644 --- a/.claude-flow/metrics/task-metrics.json +++ b/.claude-flow/metrics/task-metrics.json @@ -1,10 +1,10 @@ [ { - "id": "cmd-hooks-1763985009590", + "id": "cmd-hooks-1763991837635", "type": "hooks", "success": true, - "duration": 3.3425829999999905, - "timestamp": 1763985009594, + "duration": 8.505250000000004, + "timestamp": 1763991837644, "metadata": {} } -] \ No newline at end of file +] diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml new file mode 100644 index 0000000..d5c1872 --- /dev/null +++ b/.github/workflows/publish-pypi.yml @@ -0,0 +1,83 @@ +name: Publish to PyPI + +on: + release: + types: [published] + workflow_dispatch: # Allow manual trigger + +permissions: + contents: read + +jobs: + build: + name: Build distribution + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Check package + run: twine check dist/* + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + publish-to-pypi: + name: Publish to PyPI + needs: [build] + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/xarf + permissions: + id-token: write # Required for trusted publishing + + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + publish-to-testpypi: + name: Publish to TestPyPI + needs: [build] + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' + environment: + name: testpypi + url: https://test.pypi.org/p/xarf + + permissions: + id-token: write + + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + - name: Publish to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ diff --git a/.swarm/memory.db b/.swarm/memory.db index 992492c..1afa212 100644 Binary files a/.swarm/memory.db and b/.swarm/memory.db differ diff --git a/CHANGELOG.md b/CHANGELOG.md index bd2283b..2de4596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Changed -- **Legacy Tag Naming**: Updated v3 compatibility tags from `legacy:class:` to `legacy:category:` to align with v4 field naming conventions - - Affects only v3 report conversion metadata tags - - Maintains consistency with `category` field terminology throughout codebase +## [4.0.0] - 2025-11-30 -### Fixed -- **Documentation Examples**: Corrected CONTRIBUTING.md sample report to use `category` field instead of outdated `class` reference +### ๐ŸŽ‰ Stable Release + +XARF v4.0.0 Python parser is now production-ready! This release includes comprehensive support for all XARF v4 categories, backwards compatibility with v3, and modern Python 3.8-3.12 support. ### Added - **XARF v3 Backwards Compatibility**: Automatic conversion from v3 to v4 format @@ -27,18 +25,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Migration guide documentation at `docs/migration-guide.md` ### Changed +- **Production Status**: Updated from Beta to Production/Stable - **Pydantic V2 Migration**: Updated from Pydantic V1 to V2 API - Replaced `@validator` with `@field_validator` for all model validators - Updated `Config` class to `ConfigDict` in XARFReport model - Changed `allow_population_by_field_name` to `populate_by_name` - All validators now use `@classmethod` decorator with type hints - Fixed Python 3.13+ datetime deprecation warnings +- **Legacy Tag Naming**: Updated v3 compatibility tags from `legacy:class:` to `legacy:category:` to align with v4 field naming conventions + - Affects only v3 report conversion metadata tags + - Maintains consistency with `category` field terminology throughout codebase ### Fixed - Resolved all Pydantic V2 deprecation warnings in models - Fixed `datetime.utcnow()` deprecation by using `datetime.now(timezone.utc)` - Improved type hints for Pydantic V2 compatibility - Updated import statements to use `pydantic.ConfigDict` and `field_validator` +- **Documentation Examples**: Corrected CONTRIBUTING.md sample report to use `category` field instead of outdated `class` reference ### Documentation - Added v3 compatibility section to README with example code @@ -46,8 +49,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated feature list to highlight v3 support and Pydantic V2 compatibility - Added documentation links for migration guide -## [4.0.0] - 2024-01-20 - ### Breaking Changes #### Field Rename: `class` โ†’ `category` diff --git a/README.md b/README.md index 68dfa4d..e0c9531 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ validation_result = validate_xarf_report( - โœ… **Generation**: Create XARF v4 reports programmatically - โœ… **Evidence Handling**: Support for text, images, and binary evidence - โœ… **Category Support**: All 7 categories (messaging, connection, content, infrastructure, copyright, vulnerability, reputation) -- โœ… **Reporter Info**: Including `on_behalf_of` for infrastructure providers +- โœ… **Reporter/Sender**: Separate reporter and sender fields for third-party reporting - โœ… **XARF v3 Compatibility**: Automatic conversion with deprecation warnings - โœ… **Pydantic V2**: Modern validation with full type safety - โœ… **Python 3.8-3.12**: Full compatibility @@ -233,7 +233,12 @@ spam_report = { "reporter": { "org": "Spam Detection Service", "contact": "noreply@spamdetect.example", - "type": "automated" + "domain": "spamdetect.example" + }, + "sender": { + "org": "Spam Detection Service", + "contact": "noreply@spamdetect.example", + "domain": "spamdetect.example" }, "source_identifier": "192.0.2.100", "category": "messaging", @@ -282,30 +287,53 @@ print(f"Attack lasted {ddos_report.duration_minutes} minutes") print(f"Total packets: {ddos_report.packet_count}") ``` -### Using `on_behalf_of` for Infrastructure Providers +### Reporter vs Sender: Third-Party Reporting + +XARF v4 uses separate `reporter` and `sender` fields to distinguish between who created the report and who sent it. +**Direct Reporting** (reporter = sender): ```python from xarf.generator import XARFGenerator generator = XARFGenerator() -# Infrastructure provider (Abusix) sending report for client (Swisscom) +# Organization reporting abuse they directly observed report = generator.create_report( category="messaging", report_type="spam", source_identifier="192.0.2.150", - reporter_org="Abusix", - reporter_contact="reports@abusix.com", - on_behalf_of={ - "org": "Swisscom", - "contact": "abuse@swisscom.ch" - }, - description="Spam detected by Swisscom's infrastructure" + reporter_org="Security Team", + reporter_contact="abuse@example.com", + reporter_domain="example.com", + sender_org="Security Team", + sender_contact="abuse@example.com", + sender_domain="example.com", + description="Spam detected in our infrastructure" ) -# The report clearly shows Abusix is reporting on behalf of Swisscom print(f"Reporter: {report.reporter.org}") -print(f"On behalf of: {report.reporter.on_behalf_of.org}") +print(f"Sender: {report.sender.org}") +``` + +**Third-Party Reporting** (reporter โ‰  sender): +```python +# Infrastructure provider (Abusix) sending report on behalf of client (Swisscom) +report = generator.create_report( + category="messaging", + report_type="spam", + source_identifier="192.0.2.150", + reporter_org="Swisscom", + reporter_contact="abuse@swisscom.ch", + reporter_domain="swisscom.ch", + sender_org="Abusix", + sender_contact="reports@abusix.com", + sender_domain="abusix.com", + description="Spam detected by Swisscom, transmitted by Abusix" +) + +# The report clearly shows Swisscom is the reporter, Abusix is the sender +print(f"Reporter (who detected): {report.reporter.org}") +print(f"Sender (who transmitted): {report.sender.org}") ``` ## ๐Ÿ” Validation @@ -495,7 +523,7 @@ This project follows semantic versioning with alpha/beta releases: - `4.0.0a1`, `4.0.0a2` - Alpha releases (current) - `4.0.0b1`, `4.0.0b2` - Beta releases (planned) -- `4.0.0` - Stable release (Q2 2024) +- `4.0.0` - Stable release (Q1 2026) ## ๐ŸŽฏ Roadmap @@ -505,11 +533,11 @@ This project follows semantic versioning with alpha/beta releases: - [x] JSON schema validation - [x] messaging, connection, content categories - [x] Generator functionality -- [x] `on_behalf_of` support +- [x] Reporter/sender separation for third-party reporting - [ ] Evidence handling improvements - [ ] Performance benchmarks -### Beta Phase (Q1 2024) +### Beta Phase (Q4 2025) - [ ] Complete category coverage (all 7) - [ ] XARF v3 compatibility layer @@ -517,7 +545,7 @@ This project follows semantic versioning with alpha/beta releases: - [ ] CLI tools - [ ] Comprehensive documentation -### Stable Release (Q2 2024) +### Stable Release (Q1 2026) - [ ] Production-ready performance - [ ] >95% test coverage diff --git a/RELEASE_NOTES_4.0.0.md b/RELEASE_NOTES_4.0.0.md new file mode 100644 index 0000000..12378cd --- /dev/null +++ b/RELEASE_NOTES_4.0.0.md @@ -0,0 +1,174 @@ +# XARF v4.0.0 Python Parser - Stable Release ๐ŸŽ‰ + +We're excited to announce the **stable release** of the XARF v4.0.0 Python parser! This production-ready library provides comprehensive support for parsing, validating, and generating XARF v4 abuse reports. + +## ๐Ÿš€ What's New + +### Production Ready +- **Stable Release**: Graduated from Beta to Production/Stable +- **Python 3.8-3.12 Support**: Fully tested across all modern Python versions +- **Comprehensive Test Coverage**: 80%+ code coverage with extensive test suite +- **Type Hints**: Full type annotation support for better IDE integration + +### XARF v3 Backwards Compatibility +- **Automatic Conversion**: Seamlessly converts v3 reports to v4 format +- **Detection**: `is_v3_report()` function to identify v3 reports +- **Explicit Conversion**: `convert_v3_to_v4()` for manual conversion +- **Deprecation Warnings**: Helpful warnings when processing v3 reports +- **Migration Guide**: Complete documentation for upgrading from v3 + +### Modern Python Support +- **Pydantic V2**: Migrated to Pydantic V2 API for better performance +- **Python 3.13 Compatible**: Fixed all datetime deprecations +- **Type Safety**: Enhanced type hints throughout the codebase + +## ๐Ÿ“ฆ Installation + +```bash +pip install xarf +``` + +## ๐ŸŽฏ Quick Start + +### Parse a XARF Report + +```python +from xarf import XARFParser + +# Parse a XARF report +parser = XARFParser() +report = parser.parse('{"xarf_version": "4.0.0", "category": "messaging", ...}') + +# Access report data +print(f"Category: {report.category}") +print(f"Type: {report.type}") +print(f"Source: {report.source_identifier}") +``` + +### Generate a XARF Report + +```python +from xarf import XARFReport +from datetime import datetime, timezone + +# Create a new report +report = XARFReport( + xarf_version="4.0.0", + report_id="550e8400-e29b-41d4-a716-446655440000", + timestamp=datetime.now(timezone.utc).isoformat(), + category="connection", + type="ddos", + reporter={ + "org": "Security Operations", + "contact": "abuse@example.com", + "type": "automated" + }, + sender={ + "org": "Security Operations", + "contact": "abuse@example.com" + }, + source_identifier="192.0.2.100" +) + +# Validate and export +if report.validate(): + json_output = report.to_json(indent=2) + print(json_output) +``` + +### Convert XARF v3 to v4 + +```python +from xarf import XARFParser, is_v3_report, convert_v3_to_v4 + +# Check if report is v3 +json_data = {...} # Your XARF v3 report +if is_v3_report(json_data): + # Convert to v4 + v4_report = convert_v3_to_v4(json_data) + print(f"Converted to v4: {v4_report['category']}") + +# Or use automatic conversion +parser = XARFParser() +report = parser.parse(json_data) # Automatically converts v3 to v4 +``` + +## โœจ Features + +### Supported Categories +- โœ… **messaging** - Email spam, phishing, social engineering +- โœ… **connection** - DDoS, port scans, login attacks, brute force +- โœ… **content** - Phishing sites, malware distribution, defacement, fraud +- โœ… **infrastructure** - Compromised systems, botnets +- โœ… **copyright** - DMCA, P2P, cyberlockers +- โœ… **vulnerability** - CVE reports, misconfigurations +- โœ… **reputation** - Threat intelligence, blocklists + +### Core Capabilities +- ๐Ÿ“‹ Parse and validate XARF v4 reports +- ๐Ÿ”„ Convert XARF v3 reports to v4 format +- โœ๏ธ Generate new XARF v4 reports +- ๐Ÿ” Comprehensive JSON schema validation +- ๐Ÿ“ Type-safe Python models with Pydantic +- ๐Ÿงช Extensive test coverage (80%+) +- ๐Ÿ“š Complete documentation and examples + +## ๐Ÿ”ง Technical Details + +### Requirements +- Python 3.8 or higher +- Dependencies: + - `pydantic>=2.0.0` - Data validation + - `jsonschema>=4.0.0` - Schema validation + - `python-dateutil>=2.8.0` - Date handling + - `email-validator>=2.0.0` - Email validation + +### Breaking Changes from v3 + +#### Field Rename: `class` โ†’ `category` +The field previously named `class` has been renamed to `category` to align with the official XARF v4 specification and avoid conflicts with programming language reserved keywords. + +**Migration:** +```python +# XARF v3 +report.class_ # Old way (reserved word workaround) + +# XARF v4 +report.category # New way (clean, no workarounds) +``` + +**Automatic Conversion:** +The parser automatically converts v3 reports, so you don't need to update your existing reports immediately. A deprecation warning will guide you through the migration. + +## ๐Ÿ“– Documentation + +- **Official Website**: https://xarf.org +- **GitHub Repository**: https://github.com/xarf/xarf-python +- **XARF Specification**: https://github.com/xarf/xarf-spec +- **Migration Guide**: [docs/migration-guide.md](https://github.com/xarf/xarf-python/blob/main/docs/migration-guide.md) +- **Examples**: See the `examples/` directory + +## ๐Ÿค Contributing + +We welcome contributions! Please see: +- [CONTRIBUTING.md](https://github.com/xarf/xarf-python/blob/main/CONTRIBUTING.md) +- [CODE_OF_CONDUCT.md](https://github.com/xarf/xarf-python/blob/main/CODE_OF_CONDUCT.md) + +## ๐Ÿ“„ License + +This project is licensed under the MIT License - see the [LICENSE](https://github.com/xarf/xarf-python/blob/main/LICENSE) file for details. + +## ๐Ÿ”— Links + +- PyPI: https://pypi.org/project/xarf/ +- GitHub: https://github.com/xarf/xarf-python +- Issues: https://github.com/xarf/xarf-python/issues +- Discussions: https://github.com/xarf/xarf-spec/discussions + +## ๐Ÿ™ Acknowledgments + +Thanks to all the contributors who helped make XARF v4 possible, and to the abuse handling community for their feedback and support. + +--- + +**Full Changelog**: [CHANGELOG.md](https://github.com/xarf/xarf-python/blob/main/CHANGELOG.md) diff --git a/SECURITY.md b/SECURITY.md index 29e8e7d..c81000f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,126 +2,127 @@ ## Supported Versions -We actively support and provide security updates for the following versions: - | Version | Supported | | ------- | ------------------ | -| 2.x.x | :white_check_mark: | -| 1.x.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | ## Reporting a Vulnerability -We take security vulnerabilities seriously. If you discover a security issue in XARF Python Parser, please report it responsibly. +The XARF project takes security vulnerabilities seriously. We appreciate your efforts to responsibly disclose your findings. ### How to Report **Please DO NOT report security vulnerabilities through public GitHub issues.** -Instead, report security vulnerabilities by: +Instead, please report security vulnerabilities by emailing: -1. **Email**: Send details to security@xarf.org -2. **Private Advisory**: Use GitHub's [private security advisory feature](https://github.com/xarf/xarf-python/security/advisories/new) +**contact@xarf.org** ### What to Include -When reporting a vulnerability, please include: +Please include the following information in your report: -- Description of the vulnerability -- Steps to reproduce the issue -- Affected versions -- Potential impact assessment -- Any proof-of-concept code (if applicable) -- Your name/handle for credit (optional) +- Type of vulnerability (e.g., injection, XSS, authentication bypass) +- Full paths of source file(s) related to the vulnerability +- Location of the affected source code (tag/branch/commit or direct URL) +- Step-by-step instructions to reproduce the issue +- Proof-of-concept or exploit code (if possible) +- Impact of the issue, including how an attacker might exploit it ### Response Timeline -- **Acknowledgment**: Within 48 hours of report -- **Initial Assessment**: Within 5 business days -- **Status Updates**: Every 7 days until resolution -- **Fix Timeline**: Critical issues within 30 days, others within 90 days +- **Initial Response**: Within 48 hours +- **Status Update**: Within 7 days +- **Fix Timeline**: Depends on severity and complexity -### Disclosure Policy +### Security Update Process -- We will coordinate public disclosure with you -- Security advisories will be published after fixes are released -- We credit security researchers in advisories (unless you prefer to remain anonymous) +1. **Triage**: We'll confirm the vulnerability and assess severity +2. **Fix Development**: We'll develop and test a fix +3. **Disclosure**: We'll coordinate disclosure timing with you +4. **Release**: We'll release a security update +5. **Announcement**: We'll publish a security advisory -## Security Features +## Security Best Practices -This project implements multiple security layers: +### For Users -### Automated Scanning +1. **Keep Updated**: Always use the latest stable version +2. **Validate Input**: Never trust user-provided XARF reports without validation +3. **Size Limits**: Enforce size limits on evidence payloads (default: 5MB per item, 15MB total) +4. **Email Validation**: Use strict mode for email validation when processing reports from untrusted sources +5. **Network Exposure**: Don't expose XARF parser directly to the internet without proper validation -- **CodeQL Analysis**: Deep semantic security analysis (weekly + on PRs) -- **Dependency Review**: PR-based vulnerability scanning -- **Dependabot**: Automated dependency security updates -- **Secret Scanning**: Detects committed credentials -- **Bandit**: Python-specific security linter in CI +### For Developers -### Code Quality Gates +1. **Input Validation**: The parser validates all fields against JSON schema +2. **Email Validation**: Uses `email-validator` library for RFC-compliant validation +3. **Date Validation**: Validates timestamps against ISO 8601 format +4. **Size Limits**: Built-in limits prevent DoS via large payloads +5. **Type Safety**: Full type hints and Pydantic validation -All pull requests must pass: +## Known Security Considerations -- Static security analysis (Bandit) -- Type safety checks (MyPy strict mode) -- Dependency vulnerability scans -- Code complexity limits (Radon) +### 1. Evidence Payload Size -### Security Best Practices +XARF reports can include evidence payloads. The parser enforces: +- Maximum 5MB per evidence item +- Maximum 15MB total evidence per report -Our codebase follows: +### 2. Email Address Validation -- Strict type hints for safety -- Input validation via Pydantic models -- No hardcoded credentials -- Principle of least privilege -- Regular dependency updates +The parser uses `email-validator` for email validation. In strict mode, it performs DNS MX record checks. -## Known Security Considerations +### 3. Timestamp Handling -### XARF Report Processing +All timestamps must be in ISO 8601 format with timezone information. The parser validates format but does not check if timestamps are in the future. -When processing XARF reports: +### 4. Schema Validation -1. **Input Validation**: All reports are validated against JSON schema -2. **Email Parsing**: Uses python-email-validator for safe email processing -3. **Date Handling**: Uses python-dateutil for timezone-aware parsing -4. **No Code Execution**: Parser does not execute any user-provided code +All reports are validated against JSON schema. Invalid reports are rejected. -### Dependencies +### 5. V3 Compatibility Mode -We actively monitor and update dependencies for security issues: +When processing XARF v3 reports: +- Automatic conversion is performed +- Deprecation warnings are issued +- Original v3 data is preserved in metadata -- Automated Dependabot updates for vulnerabilities -- Grouped minor/patch updates for development dependencies -- Individual PRs for production dependency major updates +## Vulnerability Disclosure Policy -## Security Updates +We follow a **coordinated disclosure** model: -Security updates are released as: +1. **Private Disclosure**: Report sent to contact@xarf.org +2. **Acknowledgment**: We confirm receipt within 48 hours +3. **Investigation**: We investigate and develop a fix +4. **Fix Release**: We release a security update +5. **Public Disclosure**: We publish advisory 7 days after fix release -- **Critical**: Immediate patch release -- **High**: Patch release within 7 days -- **Moderate**: Included in next minor release -- **Low**: Included in next release cycle +## Security Hall of Fame -Subscribe to [GitHub Security Advisories](https://github.com/xarf/xarf-python/security/advisories) for notifications. +We recognize security researchers who responsibly disclose vulnerabilities: -## Responsible Disclosure + -We are committed to working with security researchers under responsible disclosure guidelines: +*No vulnerabilities reported yet.* -1. Allow reasonable time for fixes before public disclosure -2. Avoid privacy violations and data destruction -3. Do not exploit vulnerabilities beyond proof-of-concept -4. Respect user privacy and data protection regulations +## Bug Bounty Program -## Security Hall of Fame +Currently, we do not offer a bug bounty program. However, we deeply appreciate security research and will publicly acknowledge your contribution. + +## Contact + +- **Security Email**: contact@xarf.org +- **PGP Key**: Not yet available +- **GitHub Security Advisories**: https://github.com/xarf/xarf-python/security/advisories -We recognize security researchers who help improve our security: +## Additional Resources - +- [XARF Specification](https://xarf.org) +- [XARF Python Parser Documentation](https://github.com/xarf/xarf-python) +- [OWASP Top 10](https://owasp.org/www-project-top-ten/) --- -For general inquiries or questions about this policy, contact: security@xarf.org +**Last Updated**: 2025-11-30 diff --git a/pyproject.toml b/pyproject.toml index 658aa63..4e31914 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "xarf" -version = "4.0.0a1" +version = "4.0.0" description = "XARF v4 Python Parser - Parse and validate XARF v4 abuse reports" readme = "README.md" license = {text = "MIT"} @@ -16,7 +16,7 @@ maintainers = [ ] keywords = ["xarf", "abuse", "security", "parser", "validation"] classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "License :: OSI Approved :: MIT License", @@ -166,4 +166,4 @@ max-public-methods = 20 [tool.radon] exclude = ["tests/*", "venv/*", ".venv/*", "build/*", "dist/*"] show_complexity = true -show_mi = true \ No newline at end of file +show_mi = true diff --git a/tests/shared/samples/valid/v4/connection/auth_failure_sample.json b/tests/shared/samples/valid/v4/connection/auth_failure_sample.json index 2a13776..0e4d0fe 100644 --- a/tests/shared/samples/valid/v4/connection/auth_failure_sample.json +++ b/tests/shared/samples/valid/v4/connection/auth_failure_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Authentication Monitor", "contact": "auth-failures@secmonitor.net", - "type": "automated" + "domain": "secmonitor.net" }, "source_identifier": "172.16.0.99", "source_port": 3389, @@ -62,5 +62,10 @@ "incident_escalated": false, "remediation_applied": "ip_block" } + }, + "sender": { + "org": "Authentication Monitor", + "contact": "auth-failures@secmonitor.net", + "domain": "secmonitor.net" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/connection/ddos_certin_sample.json b/tests/shared/samples/valid/v4/connection/ddos_certin_sample.json index f351045..a3bda9b 100644 --- a/tests/shared/samples/valid/v4/connection/ddos_certin_sample.json +++ b/tests/shared/samples/valid/v4/connection/ddos_certin_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Example CERT", "contact": "incident@cert-example.org", - "type": "automated" + "domain": "cert-example.org" }, "source_identifier": "172.16.254.10", "source_port": 53, @@ -43,5 +43,10 @@ ], "traffic_volume_gbps": 15.7, "coordinator": "duty_officer_3" + }, + "sender": { + "org": "Example CERT", + "contact": "incident@cert-example.org", + "domain": "cert-example.org" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/connection/ddos_sample.json b/tests/shared/samples/valid/v4/connection/ddos_sample.json index 6291980..647c157 100644 --- a/tests/shared/samples/valid/v4/connection/ddos_sample.json +++ b/tests/shared/samples/valid/v4/connection/ddos_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "DDoS Protection Service", "contact": "ddos@protectionservice.net", - "type": "automated" + "domain": "protectionservice.net" }, "source_identifier": "192.0.2.155", "source_port": 0, @@ -56,5 +56,10 @@ "attack_vector_diversity": 3, "customer_sla_breach": false } + }, + "sender": { + "org": "DDoS Protection Service", + "contact": "ddos@protectionservice.net", + "domain": "protectionservice.net" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/connection/ip_spoof_sample.json b/tests/shared/samples/valid/v4/connection/ip_spoof_sample.json index 96406c0..cbe16cd 100644 --- a/tests/shared/samples/valid/v4/connection/ip_spoof_sample.json +++ b/tests/shared/samples/valid/v4/connection/ip_spoof_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Network Spoofing Detection", "contact": "spoofing@netdetect.org", - "type": "automated" + "domain": "netdetect.org" }, "source_identifier": "203.0.113.66", "category": "connection", @@ -54,5 +54,10 @@ "packet_analysis_depth": "headers_only", "mitigation_status": "upstream_notified" } + }, + "sender": { + "org": "Network Spoofing Detection", + "contact": "spoofing@netdetect.org", + "domain": "netdetect.org" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/connection/login_attack_sample.json b/tests/shared/samples/valid/v4/connection/login_attack_sample.json index 3602f04..c3cc29a 100644 --- a/tests/shared/samples/valid/v4/connection/login_attack_sample.json +++ b/tests/shared/samples/valid/v4/connection/login_attack_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "SSH Honeypot Network", "contact": "honeypot@sshsecurity.org", - "type": "automated" + "domain": "sshsecurity.org" }, "source_identifier": "198.51.100.77", "source_port": 45621, @@ -66,5 +66,10 @@ "compromise_attempts": "unsuccessful", "honeypot_engagement_level": "high" } + }, + "sender": { + "org": "SSH Honeypot Network", + "contact": "honeypot@sshsecurity.org", + "domain": "sshsecurity.org" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/connection/port_scan_sample.json b/tests/shared/samples/valid/v4/connection/port_scan_sample.json index 1480f1c..4f53813 100644 --- a/tests/shared/samples/valid/v4/connection/port_scan_sample.json +++ b/tests/shared/samples/valid/v4/connection/port_scan_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Network Security Monitoring", "contact": "alerts@netsec-mon.com", - "type": "automated" + "domain": "netsec-mon.com" }, "source_identifier": "198.51.100.150", "category": "connection", @@ -72,5 +72,10 @@ "threat_level": "medium", "automated_blocking": true } + }, + "sender": { + "org": "Network Security Monitoring", + "contact": "alerts@netsec-mon.com", + "domain": "netsec-mon.com" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/content/defacement_sample.json b/tests/shared/samples/valid/v4/content/defacement_sample.json index 8c4fb97..c8d4c06 100644 --- a/tests/shared/samples/valid/v4/content/defacement_sample.json +++ b/tests/shared/samples/valid/v4/content/defacement_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Website Defacement Monitor", "contact": "defacements@webmonitor.sec", - "type": "automated" + "domain": "webmonitor.sec" }, "source_identifier": "203.0.113.88", "category": "content", @@ -61,5 +61,10 @@ "law_enforcement_notified": true, "media_attention_risk": "high" } + }, + "sender": { + "org": "Website Defacement Monitor", + "contact": "defacements@webmonitor.sec", + "domain": "webmonitor.sec" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/content/fraud_sample.json b/tests/shared/samples/valid/v4/content/fraud_sample.json index b16854c..2fc3382 100644 --- a/tests/shared/samples/valid/v4/content/fraud_sample.json +++ b/tests/shared/samples/valid/v4/content/fraud_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Anti-Fraud Coalition", "contact": "fraud@antifraud.org", - "type": "automated" + "domain": "antifraud.org" }, "source_identifier": "192.0.2.211", "category": "content", @@ -63,5 +63,10 @@ ], "investigation_priority": "urgent" } + }, + "sender": { + "org": "Anti-Fraud Coalition", + "contact": "fraud@antifraud.org", + "domain": "antifraud.org" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/content/malware_distribution_sample.json b/tests/shared/samples/valid/v4/content/malware_distribution_sample.json index 72d7d68..752c3d8 100644 --- a/tests/shared/samples/valid/v4/content/malware_distribution_sample.json +++ b/tests/shared/samples/valid/v4/content/malware_distribution_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Malware Research Lab", "contact": "malware@security-research.org", - "type": "automated" + "domain": "security-research.org" }, "source_identifier": "192.0.2.75", "category": "content", @@ -53,5 +53,10 @@ "family_confidence": 0.93, "distribution_method": "drive_by_download" } + }, + "sender": { + "org": "Malware Research Lab", + "contact": "malware@security-research.org", + "domain": "security-research.org" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/content/phishing_site_lentho_sample.json b/tests/shared/samples/valid/v4/content/phishing_site_lentho_sample.json index 4e9b0a8..290d7cb 100644 --- a/tests/shared/samples/valid/v4/content/phishing_site_lentho_sample.json +++ b/tests/shared/samples/valid/v4/content/phishing_site_lentho_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Example Security Vendor", "contact": "abuse@security-vendor.example", - "type": "manual" + "domain": "security-vendor.example" }, "source_identifier": "malicious-example.net", "category": "content", @@ -51,5 +51,10 @@ "ssl_certificate_suspicious": false, "geolocation": "unknown" } + }, + "sender": { + "org": "Example Security Vendor", + "contact": "abuse@security-vendor.example", + "domain": "security-vendor.example" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/content/phishing_ybrand_sample.json b/tests/shared/samples/valid/v4/content/phishing_ybrand_sample.json index bbc3160..86184e1 100644 --- a/tests/shared/samples/valid/v4/content/phishing_ybrand_sample.json +++ b/tests/shared/samples/valid/v4/content/phishing_ybrand_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "YBrandProtection", "contact": "takedown@ybrandprotection.com", - "type": "automated" + "domain": "ybrandprotection.com" }, "source_identifier": "203.0.113.45", "category": "content", @@ -37,5 +37,10 @@ "takedown_sent": true, "response_deadline": "2024-01-17T16:45:12Z", "escalation": "legal_team" + }, + "sender": { + "org": "YBrandProtection", + "contact": "takedown@ybrandprotection.com", + "domain": "ybrandprotection.com" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/content/spamvertised_sample.json b/tests/shared/samples/valid/v4/content/spamvertised_sample.json index 38a9cb7..0148473 100644 --- a/tests/shared/samples/valid/v4/content/spamvertised_sample.json +++ b/tests/shared/samples/valid/v4/content/spamvertised_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Spam Analysis Center", "contact": "spamvertised@spamanalysis.org", - "type": "automated" + "domain": "spamanalysis.org" }, "source_identifier": "198.51.100.222", "category": "content", @@ -63,5 +63,10 @@ "regulatory_violation_score": 0.96, "takedown_urgency": "high_priority" } + }, + "sender": { + "org": "Spam Analysis Center", + "contact": "spamvertised@spamanalysis.org", + "domain": "spamanalysis.org" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/content/web_hack_sample.json b/tests/shared/samples/valid/v4/content/web_hack_sample.json index 82796a4..e9e716b 100644 --- a/tests/shared/samples/valid/v4/content/web_hack_sample.json +++ b/tests/shared/samples/valid/v4/content/web_hack_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Web Application Security Scanner", "contact": "webhacks@appsec.monitor", - "type": "automated" + "domain": "appsec.monitor" }, "source_identifier": "192.0.2.133", "category": "content", @@ -62,5 +62,10 @@ ], "incident_response_required": true } + }, + "sender": { + "org": "Web Application Security Scanner", + "contact": "webhacks@appsec.monitor", + "domain": "appsec.monitor" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/copyright/dmca_tvb_sample.json b/tests/shared/samples/valid/v4/copyright/dmca_tvb_sample.json index 2e953cc..f4d99f7 100644 --- a/tests/shared/samples/valid/v4/copyright/dmca_tvb_sample.json +++ b/tests/shared/samples/valid/v4/copyright/dmca_tvb_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "TVB Copyright Department", "contact": "copyright@tvb.com", - "type": "automated" + "domain": "tvb.com" }, "source_identifier": "198.51.100.75", "category": "copyright", @@ -39,5 +39,10 @@ "revenue_impact": "medium", "legal_review": "not_required", "content_type": "premium_episode" + }, + "sender": { + "org": "TVB Copyright Department", + "contact": "copyright@tvb.com", + "domain": "tvb.com" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/copyright/trademark_sample.json b/tests/shared/samples/valid/v4/copyright/trademark_sample.json index 3b5ef3d..e874f49 100644 --- a/tests/shared/samples/valid/v4/copyright/trademark_sample.json +++ b/tests/shared/samples/valid/v4/copyright/trademark_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Brand Protection Agency", "contact": "trademark@brandprotection.legal", - "type": "manual" + "domain": "brandprotection.legal" }, "source_identifier": "192.0.2.220", "category": "copyright", @@ -64,5 +64,10 @@ "legal_action_status": "cease_desist_sent", "brand_damage_assessment": "moderate" } + }, + "sender": { + "org": "Brand Protection Agency", + "contact": "trademark@brandprotection.legal", + "domain": "brandprotection.legal" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/examples/internal_metadata_receiver_example.json b/tests/shared/samples/valid/v4/examples/internal_metadata_receiver_example.json index ee48993..6e927c7 100644 --- a/tests/shared/samples/valid/v4/examples/internal_metadata_receiver_example.json +++ b/tests/shared/samples/valid/v4/examples/internal_metadata_receiver_example.json @@ -5,7 +5,7 @@ "reporter": { "org": "Example Anti-Spam Service", "contact": "reports@antispam-service.example", - "type": "automated" + "domain": "antispam-service.example" }, "source_identifier": "203.0.113.88", "source_port": 25, @@ -38,5 +38,10 @@ ], "billing_impact": true, "notes": "Customer contacted. Investigating compromised account." + }, + "sender": { + "org": "Example Anti-Spam Service", + "contact": "reports@antispam-service.example", + "domain": "antispam-service.example" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/examples/internal_metadata_sender_example.json b/tests/shared/samples/valid/v4/examples/internal_metadata_sender_example.json index dd67311..97f4879 100644 --- a/tests/shared/samples/valid/v4/examples/internal_metadata_sender_example.json +++ b/tests/shared/samples/valid/v4/examples/internal_metadata_sender_example.json @@ -5,7 +5,7 @@ "reporter": { "org": "Example Anti-Spam Service", "contact": "reports@antispam-service.example", - "type": "automated" + "domain": "antispam-service.example" }, "source_identifier": "203.0.113.88", "source_port": 25, @@ -37,5 +37,10 @@ "partner_b" ], "analyst_review": false + }, + "sender": { + "org": "Example Anti-Spam Service", + "contact": "reports@antispam-service.example", + "domain": "antispam-service.example" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/examples/internal_metadata_transmitted_example.json b/tests/shared/samples/valid/v4/examples/internal_metadata_transmitted_example.json index 481ba26..f5b7998 100644 --- a/tests/shared/samples/valid/v4/examples/internal_metadata_transmitted_example.json +++ b/tests/shared/samples/valid/v4/examples/internal_metadata_transmitted_example.json @@ -5,7 +5,7 @@ "reporter": { "org": "Example Anti-Spam Service", "contact": "reports@antispam-service.example", - "type": "automated" + "domain": "antispam-service.example" }, "source_identifier": "203.0.113.88", "source_port": 25, @@ -26,5 +26,10 @@ "spam:phishing", "target:banking", "language:english" - ] -} \ No newline at end of file + ], + "sender": { + "org": "Example Anti-Spam Service", + "contact": "reports@antispam-service.example", + "domain": "antispam-service.example" + } +} diff --git a/tests/shared/samples/valid/v4/examples/reporter_sender_different.json b/tests/shared/samples/valid/v4/examples/reporter_sender_different.json new file mode 100644 index 0000000..37cae0e --- /dev/null +++ b/tests/shared/samples/valid/v4/examples/reporter_sender_different.json @@ -0,0 +1,34 @@ +{ + "xarf_version": "4.0.0", + "report_id": "550e8400-e29b-41d4-a716-446655440002", + "timestamp": "2024-11-24T14:35:00Z", + "reporter": { + "org": "Security Research Lab", + "contact": "research@seclab.org", + "domain": "seclab.org" + }, + "sender": { + "org": "ISP Network Operations", + "contact": "noc@isp.net", + "domain": "isp.net" + }, + "source_identifier": "192.0.2.150", + "category": "content", + "type": "phishing_site", + "evidence_source": "researcher_analysis", + "url": "http://phishing-example.bad", + "content_type": "text/html", + "description": "Example report where reporter and sender are different organizations - researcher reporting on behalf of ISP", + "tags": [ + "phishing", + "credential_theft", + "researcher_analysis" + ], + "evidence": [ + { + "content_type": "text/plain", + "description": "Screenshot of phishing page", + "payload": "UGhpc2hpbmcgcGFnZSBzY3JlZW5zaG90IGRhdGE=" + } + ] +} diff --git a/tests/shared/samples/valid/v4/examples/reporter_sender_same.json b/tests/shared/samples/valid/v4/examples/reporter_sender_same.json new file mode 100644 index 0000000..709c88d --- /dev/null +++ b/tests/shared/samples/valid/v4/examples/reporter_sender_same.json @@ -0,0 +1,31 @@ +{ + "xarf_version": "4.0.0", + "report_id": "550e8400-e29b-41d4-a716-446655440001", + "timestamp": "2024-11-24T14:30:00Z", + "reporter": { + "org": "Example Security Team", + "contact": "abuse@example.com", + "domain": "example.com" + }, + "sender": { + "org": "Example Security Team", + "contact": "abuse@example.com", + "domain": "example.com" + }, + "source_identifier": "192.0.2.100", + "category": "connection", + "type": "ddos", + "evidence_source": "automated_scan", + "destination_ip": "203.0.113.50", + "protocol": "tcp", + "destination_port": 443, + "attack_type": "syn_flood", + "duration_minutes": 30, + "packet_count": 1500000, + "description": "Example report where reporter and sender are the same organization", + "tags": [ + "ddos", + "syn_flood", + "automated" + ] +} diff --git a/tests/shared/samples/valid/v4/infrastructure/bot_certbund_sample.json b/tests/shared/samples/valid/v4/infrastructure/bot_certbund_sample.json index 33d2d9b..489fa11 100644 --- a/tests/shared/samples/valid/v4/infrastructure/bot_certbund_sample.json +++ b/tests/shared/samples/valid/v4/infrastructure/bot_certbund_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Example CERT Organization", "contact": "cert@cert-org.example", - "type": "automated" + "domain": "cert-org.example" }, "source_identifier": "198.51.100.25", "category": "infrastructure", @@ -37,5 +37,10 @@ ], "analyst": "team_alpha", "publication_approved": false + }, + "sender": { + "org": "Example CERT Organization", + "contact": "cert@cert-org.example", + "domain": "cert-org.example" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/infrastructure/compromised_account_sample.json b/tests/shared/samples/valid/v4/infrastructure/compromised_account_sample.json index 8233951..98bf3cf 100644 --- a/tests/shared/samples/valid/v4/infrastructure/compromised_account_sample.json +++ b/tests/shared/samples/valid/v4/infrastructure/compromised_account_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Account Security Monitor", "contact": "breaches@accountsec.net", - "type": "automated" + "domain": "accountsec.net" }, "source_identifier": "198.51.100.88", "category": "infrastructure", @@ -58,5 +58,10 @@ "mfa_enabled": false, "user_notification_sent": true } + }, + "sender": { + "org": "Account Security Monitor", + "contact": "breaches@accountsec.net", + "domain": "accountsec.net" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/infrastructure/compromised_microsoft_exchange_sample.json b/tests/shared/samples/valid/v4/infrastructure/compromised_microsoft_exchange_sample.json index 4745897..55f2bbe 100644 --- a/tests/shared/samples/valid/v4/infrastructure/compromised_microsoft_exchange_sample.json +++ b/tests/shared/samples/valid/v4/infrastructure/compromised_microsoft_exchange_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Microsoft Exchange Security Team", "contact": "exchange-security@enterprise-vendor.example", - "type": "automated" + "domain": "enterprise-vendor.example" }, "source_identifier": "172.16.10.50", "source_port": 443, @@ -58,5 +58,10 @@ "microsoft_notified": true, "emergency_patching_required": true } + }, + "sender": { + "org": "Microsoft Exchange Security Team", + "contact": "exchange-security@enterprise-vendor.example", + "domain": "enterprise-vendor.example" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/infrastructure/compromised_server_sample.json b/tests/shared/samples/valid/v4/infrastructure/compromised_server_sample.json index 6fe5c11..9262217 100644 --- a/tests/shared/samples/valid/v4/infrastructure/compromised_server_sample.json +++ b/tests/shared/samples/valid/v4/infrastructure/compromised_server_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Server Security Monitor", "contact": "incidents@serversec.org", - "type": "automated" + "domain": "serversec.org" }, "source_identifier": "203.0.113.150", "source_port": 80, @@ -56,5 +56,10 @@ "incident_classification": "security_breach", "containment_required": true } + }, + "sender": { + "org": "Server Security Monitor", + "contact": "incidents@serversec.org", + "domain": "serversec.org" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/infrastructure/compromised_website_sample.json b/tests/shared/samples/valid/v4/infrastructure/compromised_website_sample.json index 8303a18..3784d88 100644 --- a/tests/shared/samples/valid/v4/infrastructure/compromised_website_sample.json +++ b/tests/shared/samples/valid/v4/infrastructure/compromised_website_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Web Security Scanner", "contact": "alerts@webscan.security", - "type": "automated" + "domain": "webscan.security" }, "source_identifier": "192.0.2.180", "category": "infrastructure", @@ -55,5 +55,10 @@ "cleanup_complexity": "moderate", "site_owner_notified": false } + }, + "sender": { + "org": "Web Security Scanner", + "contact": "alerts@webscan.security", + "domain": "webscan.security" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/infrastructure/cve_infrastructure_sample.json b/tests/shared/samples/valid/v4/infrastructure/cve_infrastructure_sample.json index adc9162..48a75c1 100644 --- a/tests/shared/samples/valid/v4/infrastructure/cve_infrastructure_sample.json +++ b/tests/shared/samples/valid/v4/infrastructure/cve_infrastructure_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Infrastructure CVE Monitor", "contact": "cve-reports@infrasec.org", - "type": "automated" + "domain": "infrasec.org" }, "source_identifier": "203.0.113.99", "source_port": 8080, @@ -61,5 +61,10 @@ "threat_actor_attribution": "unknown", "incident_response_activated": true } + }, + "sender": { + "org": "Infrastructure CVE Monitor", + "contact": "cve-reports@infrasec.org", + "domain": "infrasec.org" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/messaging/spam_spamcop_sample.json b/tests/shared/samples/valid/v4/messaging/spam_spamcop_sample.json index 43ef045..df1d5ff 100644 --- a/tests/shared/samples/valid/v4/messaging/spam_spamcop_sample.json +++ b/tests/shared/samples/valid/v4/messaging/spam_spamcop_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Example Anti-Spam Service", "contact": "reports@antispam-service.example", - "type": "automated" + "domain": "antispam-service.example" }, "source_identifier": "192.168.1.100", "source_port": 25, @@ -30,5 +30,10 @@ "_internal": { "batch_id": "20240115_001", "processed": "2024-01-15T14:30:25Z" + }, + "sender": { + "org": "Example Anti-Spam Service", + "contact": "reports@antispam-service.example", + "domain": "antispam-service.example" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/messaging/spam_spamtrap_phishing_sample.json b/tests/shared/samples/valid/v4/messaging/spam_spamtrap_phishing_sample.json index 3db01c7..1bf5f18 100644 --- a/tests/shared/samples/valid/v4/messaging/spam_spamtrap_phishing_sample.json +++ b/tests/shared/samples/valid/v4/messaging/spam_spamtrap_phishing_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Abusix", "contact": "abuse@abusix.com", - "type": "automated" + "domain": "abusix.com" }, "source_identifier": "35.243.100.5", "source_port": 25, @@ -31,5 +31,10 @@ "malicious_link:hykwon.com", "severity:high" ], - "description": "Sophisticated phishing email impersonating SBI Securities (Japanese financial company), claiming account security issues and directing to malicious domain hykwon.com" -} \ No newline at end of file + "description": "Sophisticated phishing email impersonating SBI Securities (Japanese financial company), claiming account security issues and directing to malicious domain hykwon.com", + "sender": { + "org": "Abusix", + "contact": "abuse@abusix.com", + "domain": "abusix.com" + } +} diff --git a/tests/shared/samples/valid/v4/messaging/spam_user_complaint_sample.json b/tests/shared/samples/valid/v4/messaging/spam_user_complaint_sample.json index 1822621..6bbff06 100644 --- a/tests/shared/samples/valid/v4/messaging/spam_user_complaint_sample.json +++ b/tests/shared/samples/valid/v4/messaging/spam_user_complaint_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Example Security", "contact": "abuse@example.com", - "type": "manual" + "domain": "example.com" }, "source_identifier": "209.85.220.65", "source_port": 25, @@ -49,5 +49,10 @@ "user_report_method": "web_form", "complaint_category": "419_advance_fee" } + }, + "sender": { + "org": "Example Security", + "contact": "abuse@example.com", + "domain": "example.com" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/messaging/spam_v3_converted_sample.json b/tests/shared/samples/valid/v4/messaging/spam_v3_converted_sample.json index 7e8e111..bff446a 100644 --- a/tests/shared/samples/valid/v4/messaging/spam_v3_converted_sample.json +++ b/tests/shared/samples/valid/v4/messaging/spam_v3_converted_sample.json @@ -6,7 +6,7 @@ "reporter": { "org": "Legacy Spam Reporter", "contact": "abuse@legacy-reporter.com", - "type": "unknown" + "domain": "legacy-reporter.com" }, "source_identifier": "203.0.113.75", "source_port": 25, @@ -45,5 +45,10 @@ "conversion_timestamp": "2024-01-15T12:00:00Z", "original_format": "xarf_v3" } + }, + "sender": { + "org": "Legacy Spam Reporter", + "contact": "abuse@legacy-reporter.com", + "domain": "legacy-reporter.com" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/messaging/whatsapp_social_engineering_sample.json b/tests/shared/samples/valid/v4/messaging/whatsapp_social_engineering_sample.json index f6f6060..209645f 100644 --- a/tests/shared/samples/valid/v4/messaging/whatsapp_social_engineering_sample.json +++ b/tests/shared/samples/valid/v4/messaging/whatsapp_social_engineering_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Mobile Security Research", "contact": "whatsapp-abuse@mobile-security.org", - "type": "manual" + "domain": "mobile-security.org" }, "source_identifier": "+447955527026", "category": "messaging", @@ -44,5 +44,10 @@ "phone_verification": "unverified_uk_number", "social_graph_analysis": "no_mutual_contacts", "response_action": "blocked_and_reported_to_whatsapp" + }, + "sender": { + "org": "Mobile Security Research", + "contact": "whatsapp-abuse@mobile-security.org", + "domain": "mobile-security.org" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/reputation/blocklist_aggregated_sample.json b/tests/shared/samples/valid/v4/reputation/blocklist_aggregated_sample.json index 33dccc5..74db3ab 100644 --- a/tests/shared/samples/valid/v4/reputation/blocklist_aggregated_sample.json +++ b/tests/shared/samples/valid/v4/reputation/blocklist_aggregated_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Threat Intelligence Consortium", "contact": "intel@threatconsortium.org", - "type": "automated" + "domain": "threatconsortium.org" }, "source_identifier": "203.0.113.200", "category": "reputation", @@ -54,5 +54,10 @@ "automated_sharing": true, "intel_classification": "tlp_amber" } + }, + "sender": { + "org": "Threat Intelligence Consortium", + "contact": "intel@threatconsortium.org", + "domain": "threatconsortium.org" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/reputation/ip_reclamation_sample.json b/tests/shared/samples/valid/v4/reputation/ip_reclamation_sample.json index e5f0aab..f88d829 100644 --- a/tests/shared/samples/valid/v4/reputation/ip_reclamation_sample.json +++ b/tests/shared/samples/valid/v4/reputation/ip_reclamation_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "IP Reputation Service", "contact": "reclamation@ipreputation.org", - "type": "manual" + "domain": "ipreputation.org" }, "source_identifier": "203.0.113.175", "category": "reputation", @@ -70,5 +70,10 @@ "reputation_restoration_status": "approved", "blocklist_removal_requests": 12 } + }, + "sender": { + "org": "IP Reputation Service", + "contact": "reclamation@ipreputation.org", + "domain": "ipreputation.org" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/reputation/trap_sample.json b/tests/shared/samples/valid/v4/reputation/trap_sample.json index 64967c8..6fec9a6 100644 --- a/tests/shared/samples/valid/v4/reputation/trap_sample.json +++ b/tests/shared/samples/valid/v4/reputation/trap_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Honeypot Research Network", "contact": "traps@honeypot-research.org", - "type": "automated" + "domain": "honeypot-research.org" }, "source_identifier": "198.51.100.199", "category": "reputation", @@ -63,5 +63,10 @@ "threat_intelligence_value": "medium", "ioc_extraction_success": true } + }, + "sender": { + "org": "Honeypot Research Network", + "contact": "traps@honeypot-research.org", + "domain": "honeypot-research.org" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/vulnerability/cve_sample.json b/tests/shared/samples/valid/v4/vulnerability/cve_sample.json index a73e804..f5841f2 100644 --- a/tests/shared/samples/valid/v4/vulnerability/cve_sample.json +++ b/tests/shared/samples/valid/v4/vulnerability/cve_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Vulnerability Assessment Service", "contact": "vulns@assessment-service.org", - "type": "automated" + "domain": "assessment-service.org" }, "source_identifier": "172.16.1.200", "source_port": 8080, @@ -48,5 +48,10 @@ ], "last_patched": "2023-10-15T09:30:00Z", "vuln_age_days": 92 + }, + "sender": { + "org": "Vulnerability Assessment Service", + "contact": "vulns@assessment-service.org", + "domain": "assessment-service.org" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/vulnerability/malicious_activity_sample.json b/tests/shared/samples/valid/v4/vulnerability/malicious_activity_sample.json index 2b8ce28..6f3d524 100644 --- a/tests/shared/samples/valid/v4/vulnerability/malicious_activity_sample.json +++ b/tests/shared/samples/valid/v4/vulnerability/malicious_activity_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "Malicious Activity Monitor", "contact": "malicious@activitymonitor.sec", - "type": "automated" + "domain": "activitymonitor.sec" }, "source_identifier": "172.16.55.200", "source_port": 8080, @@ -61,5 +61,10 @@ "incident_response_triggered": true, "forensic_evidence_preserved": true } + }, + "sender": { + "org": "Malicious Activity Monitor", + "contact": "malicious@activitymonitor.sec", + "domain": "activitymonitor.sec" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/vulnerability/open_service_shadowserver_sample.json b/tests/shared/samples/valid/v4/vulnerability/open_service_shadowserver_sample.json index f77242f..b886d28 100644 --- a/tests/shared/samples/valid/v4/vulnerability/open_service_shadowserver_sample.json +++ b/tests/shared/samples/valid/v4/vulnerability/open_service_shadowserver_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "The Shadowserver Foundation", "contact": "reports@threat-intel.example", - "type": "automated" + "domain": "threat-intel.example" }, "source_identifier": "192.0.2.50", "source_port": 3389, @@ -50,5 +50,10 @@ "exposure_duration_days": 42, "remediation_urgency": "high" } + }, + "sender": { + "org": "The Shadowserver Foundation", + "contact": "reports@threat-intel.example", + "domain": "threat-intel.example" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/vulnerability/outdated_dnssec_sample.json b/tests/shared/samples/valid/v4/vulnerability/outdated_dnssec_sample.json index 1f97ee9..bf434ae 100644 --- a/tests/shared/samples/valid/v4/vulnerability/outdated_dnssec_sample.json +++ b/tests/shared/samples/valid/v4/vulnerability/outdated_dnssec_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "DNSSEC Monitoring Service", "contact": "dnssec@domainmonitor.org", - "type": "automated" + "domain": "domainmonitor.org" }, "source_identifier": "203.0.113.200", "source_port": 53, @@ -56,5 +56,10 @@ "security_incident": false, "compliance_impact": "moderate" } + }, + "sender": { + "org": "DNSSEC Monitoring Service", + "contact": "dnssec@domainmonitor.org", + "domain": "domainmonitor.org" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/vulnerability/ssl_freak_sample.json b/tests/shared/samples/valid/v4/vulnerability/ssl_freak_sample.json index 930372d..521652e 100644 --- a/tests/shared/samples/valid/v4/vulnerability/ssl_freak_sample.json +++ b/tests/shared/samples/valid/v4/vulnerability/ssl_freak_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "SSL Vulnerability Scanner", "contact": "ssl-vulns@tlsmonitor.org", - "type": "automated" + "domain": "tlsmonitor.org" }, "source_identifier": "192.0.2.88", "source_port": 443, @@ -57,5 +57,10 @@ "remediation_effort": "configuration_change", "pci_compliance_impact": true } + }, + "sender": { + "org": "SSL Vulnerability Scanner", + "contact": "ssl-vulns@tlsmonitor.org", + "domain": "tlsmonitor.org" } -} \ No newline at end of file +} diff --git a/tests/shared/samples/valid/v4/vulnerability/ssl_poodle_sample.json b/tests/shared/samples/valid/v4/vulnerability/ssl_poodle_sample.json index f4385d1..4605d11 100644 --- a/tests/shared/samples/valid/v4/vulnerability/ssl_poodle_sample.json +++ b/tests/shared/samples/valid/v4/vulnerability/ssl_poodle_sample.json @@ -5,7 +5,7 @@ "reporter": { "org": "TLS Security Assessment", "contact": "poodle@tlsassess.org", - "type": "automated" + "domain": "tlsassess.org" }, "source_identifier": "198.51.100.134", "source_port": 443, @@ -59,5 +59,10 @@ "legacy_browser_support": true, "remediation_impact": "minimal" } + }, + "sender": { + "org": "TLS Security Assessment", + "contact": "poodle@tlsassess.org", + "domain": "tlsassess.org" } -} \ No newline at end of file +} diff --git a/tests/test_generator.py b/tests/test_generator.py index c2560c4..b6c74ca 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -1,9 +1,9 @@ -"""Tests for XARF Report Generator (if implemented).""" +"""Tests for XARF Report Generator.""" import uuid from datetime import datetime, timezone -from xarf.models import MessagingReport, XARFReporter +from xarf.models import ContactInfo, MessagingReport class TestReportGeneration: @@ -11,8 +11,11 @@ class TestReportGeneration: def test_create_messaging_report(self): """Test creating a messaging report programmatically.""" - reporter = XARFReporter( - org="Test Organization", contact="abuse@test.com", type="automated" + reporter = ContactInfo( + org="Test Organization", contact="abuse@test.com", domain="test.com" + ) + sender = ContactInfo( + org="Test Organization", contact="abuse@test.com", domain="test.com" ) report = MessagingReport( @@ -20,6 +23,7 @@ def test_create_messaging_report(self): report_id=str(uuid.uuid4()), timestamp=datetime.now(timezone.utc), reporter=reporter, + sender=sender, source_identifier="192.0.2.1", category="messaging", type="spam", diff --git a/tests/test_parser.py b/tests/test_parser.py index 9c52568..ad2f828 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -20,7 +20,12 @@ def test_parse_valid_messaging_report(self): "reporter": { "org": "Test Org", "contact": "test@example.com", - "type": "automated", + "domain": "example.com", + }, + "sender": { + "org": "Test Org", + "contact": "test@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.100", "category": "messaging", @@ -48,7 +53,12 @@ def test_parse_valid_connection_report(self): "reporter": { "org": "Security Monitor", "contact": "security@example.com", - "type": "automated", + "domain": "example.com", + }, + "sender": { + "org": "Security Monitor", + "contact": "security@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.200", "category": "connection", @@ -77,7 +87,12 @@ def test_parse_valid_content_report(self): "reporter": { "org": "Web Security", "contact": "web@example.com", - "type": "manual", + "domain": "example.com", + }, + "sender": { + "org": "Web Security", + "contact": "web@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.300", "category": "content", @@ -103,7 +118,12 @@ def test_parse_json_string(self): "reporter": { "org": "Test", "contact": "test@example.com", - "type": "automated", + "domain": "example.com", + }, + "sender": { + "org": "Test", + "contact": "test@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.1", "category": "messaging", @@ -126,7 +146,12 @@ def test_validation_errors(self): "reporter": { "org": "Test", "contact": "test@example.com", - "type": "automated", + "domain": "example.com", + }, + "sender": { + "org": "Test", + "contact": "test@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.1", "category": "messaging", @@ -170,7 +195,12 @@ def test_unsupported_category_alpha(self): "reporter": { "org": "Test", "contact": "test@example.com", - "type": "automated", + "domain": "example.com", + }, + "sender": { + "org": "Test", + "contact": "test@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.1", "category": "vulnerability", # Not supported in alpha @@ -201,8 +231,8 @@ def test_missing_required_fields(self): errors = parser.get_errors() assert any("Missing required fields" in error for error in errors) - def test_invalid_reporter_type(self): - """Test invalid reporter type validation.""" + def test_missing_sender(self): + """Test validation fails without sender field.""" invalid_data = { "xarf_version": "4.0.0", "report_id": "test-id", @@ -210,8 +240,9 @@ def test_invalid_reporter_type(self): "reporter": { "org": "Test", "contact": "test@example.com", - "type": "invalid_type", # Invalid + "domain": "example.com", }, + # Missing sender "source_identifier": "192.0.2.1", "category": "messaging", "type": "spam", @@ -223,4 +254,62 @@ def test_invalid_reporter_type(self): assert result is False errors = parser.get_errors() - assert any("Invalid reporter type" in error for error in errors) + assert any("sender" in error.lower() for error in errors) + + def test_reporter_sender_same_org(self): + """Test report where reporter and sender are the same.""" + report_data = { + "xarf_version": "4.0.0", + "report_id": "test-same-org", + "timestamp": "2024-01-15T10:30:00Z", + "reporter": { + "org": "Same Org", + "contact": "abuse@same.com", + "domain": "same.com", + }, + "sender": { + "org": "Same Org", + "contact": "abuse@same.com", + "domain": "same.com", + }, + "source_identifier": "192.0.2.1", + "category": "messaging", + "type": "spam", + "evidence_source": "spamtrap", + } + + parser = XARFParser() + report = parser.parse(report_data) + + assert report.reporter.org == "Same Org" + assert report.sender.org == "Same Org" + assert report.reporter.org == report.sender.org + + def test_reporter_sender_different_orgs(self): + """Test report where reporter and sender are different organizations.""" + report_data = { + "xarf_version": "4.0.0", + "report_id": "test-diff-org", + "timestamp": "2024-01-15T10:30:00Z", + "reporter": { + "org": "Reporter Org", + "contact": "abuse@reporter.com", + "domain": "reporter.com", + }, + "sender": { + "org": "Sender Org", + "contact": "abuse@sender.com", + "domain": "sender.com", + }, + "source_identifier": "192.0.2.1", + "category": "messaging", + "type": "spam", + "evidence_source": "spamtrap", + } + + parser = XARFParser() + report = parser.parse(report_data) + + assert report.reporter.org == "Reporter Org" + assert report.sender.org == "Sender Org" + assert report.reporter.org != report.sender.org diff --git a/tests/test_security.py b/tests/test_security.py index 4182523..243f1d0 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -19,7 +19,12 @@ def test_valid_uuid_v4_format(self): "reporter": { "org": "Test Org", "contact": "test@example.com", - "type": "automated", + "domain": "example.com", + }, + "sender": { + "org": "Test Org", + "contact": "test@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.1", "category": "messaging", @@ -87,7 +92,12 @@ def test_report_id_string_format(self): "reporter": { "org": "Test", "contact": "test@example.com", - "type": "automated", + "domain": "example.com", + }, + "sender": { + "org": "Test", + "contact": "test@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.1", "category": "messaging", @@ -114,7 +124,12 @@ def test_iso8601_utc_format(self): "reporter": { "org": "Test", "contact": "test@example.com", - "type": "automated", + "domain": "example.com", + }, + "sender": { + "org": "Test", + "contact": "test@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.1", "category": "messaging", @@ -135,7 +150,12 @@ def test_timestamp_with_timezone(self): "reporter": { "org": "Test", "contact": "test@example.com", - "type": "automated", + "domain": "example.com", + }, + "sender": { + "org": "Test", + "contact": "test@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.1", "category": "messaging", @@ -156,7 +176,12 @@ def test_timestamp_microseconds(self): "reporter": { "org": "Test", "contact": "test@example.com", - "type": "automated", + "domain": "example.com", + }, + "sender": { + "org": "Test", + "contact": "test@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.1", "category": "messaging", @@ -188,7 +213,12 @@ def test_invalid_timestamp_format(self): "reporter": { "org": "Test", "contact": "test@example.com", - "type": "automated", + "domain": "example.com", + }, + "sender": { + "org": "Test", + "contact": "test@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.1", "category": "messaging", @@ -220,7 +250,12 @@ def test_timestamp_immutability(self): "reporter": { "org": "Test", "contact": "test@example.com", - "type": "automated", + "domain": "example.com", + }, + "sender": { + "org": "Test", + "contact": "test@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.1", "category": "messaging", @@ -252,7 +287,12 @@ def test_future_timestamp_detection(self): "reporter": { "org": "Test", "contact": "test@example.com", - "type": "automated", + "domain": "example.com", + }, + "sender": { + "org": "Test", + "contact": "test@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.1", "category": "messaging", @@ -277,7 +317,12 @@ def test_timestamp_precision(self): "reporter": { "org": "Test", "contact": "test@example.com", - "type": "automated", + "domain": "example.com", + }, + "sender": { + "org": "Test", + "contact": "test@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.1", "category": "messaging", @@ -314,7 +359,12 @@ def test_sql_injection_in_report_id(self): "reporter": { "org": "Test", "contact": "test@example.com", - "type": "automated", + "domain": "example.com", + }, + "sender": { + "org": "Test", + "contact": "test@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.1", "category": "messaging", @@ -338,7 +388,12 @@ def test_extremely_long_uuid(self): "reporter": { "org": "Test", "contact": "test@example.com", - "type": "automated", + "domain": "example.com", + }, + "sender": { + "org": "Test", + "contact": "test@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.1", "category": "messaging", @@ -360,7 +415,12 @@ def test_null_byte_injection(self): "reporter": { "org": "Test\x00Org", "contact": "test@example.com", - "type": "automated", + "domain": "example.com", + }, + "sender": { + "org": "Test\x00Org", + "contact": "test@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.1", "category": "messaging", diff --git a/tests/test_v3_compatibility.py b/tests/test_v3_compatibility.py index 5906a73..932417f 100644 --- a/tests/test_v3_compatibility.py +++ b/tests/test_v3_compatibility.py @@ -87,7 +87,7 @@ def test_convert_v3_spam_report(self): # Verify reporter assert v4_report["reporter"]["org"] == "Example Anti-Spam" assert v4_report["reporter"]["contact"] == "abuse@example.com" - assert v4_report["reporter"]["type"] == "automated" + assert v4_report["reporter"]["domain"] # Domain should be present # Verify messaging-specific fields assert v4_report["protocol"] == "smtp" diff --git a/tests/test_validation.py b/tests/test_validation.py index 79b49e7..ac92bb1 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -15,7 +15,12 @@ def test_messaging_category_valid(self): "reporter": { "org": "Email Provider", "contact": "abuse@emailprovider.com", - "type": "automated", + "domain": "emailprovider.com", + }, + "sender": { + "org": "Email Provider", + "contact": "abuse@emailprovider.com", + "domain": "emailprovider.com", }, "source_identifier": "192.0.2.1", "category": "messaging", @@ -37,7 +42,12 @@ def test_connection_category_valid(self): "reporter": { "org": "Network Monitor", "contact": "security@network.com", - "type": "automated", + "domain": "network.com", + }, + "sender": { + "org": "Network Monitor", + "contact": "security@network.com", + "domain": "network.com", }, "source_identifier": "192.0.2.2", "category": "connection", @@ -61,7 +71,12 @@ def test_content_category_valid(self): "reporter": { "org": "Web Security", "contact": "security@websec.com", - "type": "manual", + "domain": "websec.com", + }, + "sender": { + "org": "Web Security", + "contact": "security@websec.com", + "domain": "websec.com", }, "source_identifier": "192.0.2.3", "category": "content", @@ -84,7 +99,12 @@ def test_infrastructure_category_valid(self): "reporter": { "org": "Security Research", "contact": "research@security.com", - "type": "automated", + "domain": "security.com", + }, + "sender": { + "org": "Security Research", + "contact": "research@security.com", + "domain": "security.com", }, "source_identifier": "192.0.2.4", "category": "infrastructure", @@ -108,7 +128,12 @@ def test_copyright_category_valid(self): "reporter": { "org": "Copyright Holder", "contact": "legal@copyright.com", - "type": "manual", + "domain": "copyright.com", + }, + "sender": { + "org": "Copyright Holder", + "contact": "legal@copyright.com", + "domain": "copyright.com", }, "source_identifier": "192.0.2.5", "category": "copyright", @@ -129,7 +154,12 @@ def test_vulnerability_category_valid(self): "reporter": { "org": "Vulnerability Scanner", "contact": "vuln@scanner.com", - "type": "automated", + "domain": "scanner.com", + }, + "sender": { + "org": "Vulnerability Scanner", + "contact": "vuln@scanner.com", + "domain": "scanner.com", }, "source_identifier": "192.0.2.6", "category": "vulnerability", @@ -150,7 +180,12 @@ def test_reputation_category_valid(self): "reporter": { "org": "Reputation Service", "contact": "rep@service.com", - "type": "automated", + "domain": "service.com", + }, + "sender": { + "org": "Reputation Service", + "contact": "rep@service.com", + "domain": "service.com", }, "source_identifier": "192.0.2.7", "category": "reputation", @@ -171,7 +206,12 @@ def test_other_category_valid(self): "reporter": { "org": "Other Reporter", "contact": "other@reporter.com", - "type": "manual", + "domain": "reporter.com", + }, + "sender": { + "org": "Other Reporter", + "contact": "other@reporter.com", + "domain": "reporter.com", }, "source_identifier": "192.0.2.8", "category": "other", @@ -196,7 +236,12 @@ def get_valid_base_report(self): "reporter": { "org": "Test Organization", "contact": "abuse@test.com", - "type": "automated", + "domain": "test.com", + }, + "sender": { + "org": "Test Organization", + "contact": "abuse@test.com", + "domain": "test.com", }, "source_identifier": "192.0.2.1", "category": "messaging", @@ -332,27 +377,29 @@ def test_missing_reporter_contact(self): assert result is False - def test_missing_reporter_type(self): - """Test validation fails without reporter.type.""" + def test_missing_reporter_domain(self): + """Test validation fails without reporter.domain.""" report_data = self.get_valid_base_report() - del report_data["reporter"]["type"] + del report_data["reporter"]["domain"] parser = XARFParser(strict=False) result = parser.validate(report_data) assert result is False + errors = parser.get_errors() + assert any("Missing reporter fields" in error for error in errors) - def test_invalid_reporter_type(self): - """Test validation fails with invalid reporter.type.""" + def test_missing_sender_domain(self): + """Test validation fails without sender.domain.""" report_data = self.get_valid_base_report() - report_data["reporter"]["type"] = "invalid" + del report_data["sender"]["domain"] parser = XARFParser(strict=False) result = parser.validate(report_data) assert result is False errors = parser.get_errors() - assert any("Invalid reporter type" in error for error in errors) + assert any("Missing sender fields" in error for error in errors) class TestCategorySpecificFields: @@ -367,7 +414,12 @@ def test_messaging_missing_protocol(self): "reporter": { "org": "Test", "contact": "test@example.com", - "type": "automated", + "domain": "example.com", + }, + "sender": { + "org": "Test", + "contact": "test@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.1", "category": "messaging", @@ -393,7 +445,12 @@ def test_connection_missing_destination_ip(self): "reporter": { "org": "Test", "contact": "test@example.com", - "type": "automated", + "domain": "example.com", + }, + "sender": { + "org": "Test", + "contact": "test@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.1", "category": "connection", @@ -418,7 +475,12 @@ def test_content_missing_url(self): "reporter": { "org": "Test", "contact": "test@example.com", - "type": "manual", + "domain": "example.com", + }, + "sender": { + "org": "Test", + "contact": "test@example.com", + "domain": "example.com", }, "source_identifier": "192.0.2.1", "category": "content", diff --git a/xarf/generator.py b/xarf/generator.py index 3341070..f3596b0 100644 --- a/xarf/generator.py +++ b/xarf/generator.py @@ -246,11 +246,13 @@ def generate_report( category: str, report_type: str, source_identifier: str, + reporter_org: str, reporter_contact: str, - reporter_org: Optional[str] = None, - reporter_type: str = "automated", + reporter_domain: str, + sender_org: str, + sender_contact: str, + sender_domain: str, evidence_source: str = "automated_scan", - on_behalf_of: Optional[Dict[str, str]] = None, description: Optional[str] = None, evidence: Optional[List[Dict[str, str]]] = None, severity: Optional[str] = None, @@ -266,12 +268,13 @@ def generate_report( category: Report category (e.g., "connection", "content"). report_type: Specific type within category (e.g., "ddos", "phishing"). source_identifier: Source IP address or identifier. + reporter_org: Organization name of the reporter. reporter_contact: Contact email for the reporter. - reporter_org: Organization name of the reporter (optional). - reporter_type: Type of reporter (default: "automated"). + reporter_domain: Domain of the reporter organization. + sender_org: Organization name of the sender. + sender_contact: Contact email for the sender. + sender_domain: Domain of the sender organization. evidence_source: How the evidence was collected (default: "automated_scan"). - on_behalf_of: Dictionary with "org" and optional "contact" keys for - reporting on behalf of another entity. description: Human-readable description of the incident. evidence: List of evidence items (dictionaries with content_type, description, payload, and hash). @@ -294,8 +297,12 @@ def generate_report( ... category="connection", ... report_type="ddos", ... source_identifier="192.0.2.100", - ... reporter_contact="abuse@example.com", ... reporter_org="Example Security", + ... reporter_contact="abuse@example.com", + ... reporter_domain="example.com", + ... sender_org="Example Security", + ... sender_contact="abuse@example.com", + ... sender_domain="example.com", ... severity="high" ... ) >>> report["xarf_version"] @@ -304,8 +311,18 @@ def generate_report( # Validate required parameters if not source_identifier: raise XARFError("source_identifier is required") + if not reporter_org: + raise XARFError("reporter_org is required") if not reporter_contact: raise XARFError("reporter_contact is required") + if not reporter_domain: + raise XARFError("reporter_domain is required") + if not sender_org: + raise XARFError("sender_org is required") + if not sender_contact: + raise XARFError("sender_contact is required") + if not sender_domain: + raise XARFError("sender_domain is required") # Validate category if category not in self.VALID_CATEGORIES: @@ -322,13 +339,6 @@ def generate_report( f"Must be one of: {', '.join(valid_types)}" ) - # Validate reporter_type - if reporter_type not in self.VALID_REPORTER_TYPES: - raise XARFError( - f"Invalid reporter_type '{reporter_type}'. Must be one of: " - f"{', '.join(sorted(self.VALID_REPORTER_TYPES))}" - ) - # Validate evidence_source if evidence_source not in self.VALID_EVIDENCE_SOURCES: raise XARFError( @@ -352,23 +362,22 @@ def generate_report( "xarf_version": self.XARF_VERSION, "report_id": self.generate_uuid(), "timestamp": self.generate_timestamp(), - "reporter": {"contact": reporter_contact, "type": reporter_type}, + "reporter": { + "org": reporter_org, + "contact": reporter_contact, + "domain": reporter_domain, + }, + "sender": { + "org": sender_org, + "contact": sender_contact, + "domain": sender_domain, + }, "source_identifier": source_identifier, "category": category, "type": report_type, "evidence_source": evidence_source, } - # Add optional reporter fields - if reporter_org: - report["reporter"]["org"] = reporter_org - - # Add on_behalf_of if provided - if on_behalf_of: - if "org" not in on_behalf_of: - raise XARFError("on_behalf_of must contain 'org' key") - report["reporter"]["on_behalf_of"] = on_behalf_of - # Add optional fields if description: report["description"] = description @@ -484,15 +493,25 @@ def generate_sample_report( reporter_org = secrets.choice(sample_orgs) sample_domains = ["example.com", "security.net", "abuse.org", "soc.io"] - reporter_contact = f"abuse@{secrets.choice(sample_domains)}" + reporter_domain = secrets.choice(sample_domains) + reporter_contact = f"abuse@{reporter_domain}" + + # For sample reports, sender can be the same as reporter + sender_org = reporter_org + sender_domain = reporter_domain + sender_contact = reporter_contact # Build report parameters params: Dict[str, Any] = { "category": category, "report_type": report_type, "source_identifier": source_ip, - "reporter_contact": reporter_contact, "reporter_org": reporter_org, + "reporter_contact": reporter_contact, + "reporter_domain": reporter_domain, + "sender_org": sender_org, + "sender_contact": sender_contact, + "sender_domain": sender_domain, "description": f"Sample {report_type} report for testing", } diff --git a/xarf/models.py b/xarf/models.py index 826f9d8..d9d94cd 100644 --- a/xarf/models.py +++ b/xarf/models.py @@ -6,8 +6,16 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator +class ContactInfo(BaseModel): + """XARF Contact Information for reporter and sender.""" + + org: str + contact: str + domain: str + + class XARFReporter(BaseModel): - """XARF Reporter information.""" + """XARF Reporter information (deprecated, use ContactInfo).""" org: str contact: str @@ -29,8 +37,8 @@ class XARFReport(BaseModel): xarf_version: str = Field(..., pattern="^4\\.0\\.0$") report_id: str timestamp: datetime - reporter: XARFReporter - on_behalf_of: Optional[XARFReporter] = None + reporter: ContactInfo + sender: ContactInfo source_identifier: str category: str = Field(..., alias="category") type: str diff --git a/xarf/parser.py b/xarf/parser.py index eb86190..94317c7 100644 --- a/xarf/parser.py +++ b/xarf/parser.py @@ -129,6 +129,7 @@ def validate_structure(self, data: Dict[str, Any]) -> bool: "report_id", "timestamp", "reporter", + "sender", "source_identifier", "category", "type", @@ -152,15 +153,22 @@ def validate_structure(self, data: Dict[str, Any]) -> bool: self.errors.append("Reporter must be an object") return False - reporter_required = {"org", "contact", "type"} + reporter_required = {"org", "contact", "domain"} missing_reporter = reporter_required - set(reporter.keys()) if missing_reporter: self.errors.append(f"Missing reporter fields: {missing_reporter}") return False - # Validate reporter type - if reporter.get("type") not in ["automated", "manual", "hybrid"]: - self.errors.append(f"Invalid reporter type: {reporter.get('type')}") + # Validate sender structure + sender = data.get("sender", {}) + if not isinstance(sender, dict): + self.errors.append("Sender must be an object") + return False + + sender_required = {"org", "contact", "domain"} + missing_sender = sender_required - set(sender.keys()) + if missing_sender: + self.errors.append(f"Missing sender fields: {missing_sender}") return False # Validate timestamp format diff --git a/xarf/v3_compat.py b/xarf/v3_compat.py index 8472aa2..bc5b272 100644 --- a/xarf/v3_compat.py +++ b/xarf/v3_compat.py @@ -73,6 +73,18 @@ def convert_v3_to_v4(v3_data: Dict[str, Any]) -> Dict[str, Any]: # Map v3 ReportType to v4 type report_type = report.get("ReportType", "").lower() + # Extract reporter info + reporter_org = reporter_info.get("ReporterOrg", "Unknown") + reporter_contact = ( + reporter_info.get("ReporterOrgEmail") + or reporter_info.get("ReporterContactEmail") + or "unknown@example.com" + ) + # Extract domain from email if possible + reporter_domain = ( + reporter_contact.split("@")[-1] if "@" in reporter_contact else "example.com" + ) + # Build base v4 structure v4_data: Dict[str, Any] = { "xarf_version": "4.0.0", @@ -80,13 +92,14 @@ def convert_v3_to_v4(v3_data: Dict[str, Any]) -> Dict[str, Any]: "timestamp": report.get("Date") or datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), "reporter": { - "org": reporter_info.get("ReporterOrg", "Unknown"), - "contact": ( - reporter_info.get("ReporterOrgEmail") - or reporter_info.get("ReporterContactEmail") - or "unknown@example.com" - ), - "type": "automated", # v3 didn't distinguish, assume automated + "org": reporter_org, + "contact": reporter_contact, + "domain": reporter_domain, + }, + "sender": { + "org": reporter_org, + "contact": reporter_contact, + "domain": reporter_domain, }, "source_identifier": source.get("IP", "0.0.0.0"), # nosec B104 "category": category,