diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index d2e9fb7..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,83 +0,0 @@ -# Changelog for STIX Bundle Generator (index.html) - -All notable changes to the `index.html` artifact (`artifact_id="f8b52830-61e7-4dcd-b9f3-809e7d8ff123"`) are documented in this file. Versions are listed in descending order, with the latest version at the top. - -## Version 8b9f7a2c-5e3d-4f1a-9c2b-3f4e5d6f7890 - 2025-05-19 20:53 CEST -- **Added**: New attributes to `x509-certificate` SCO: `issuer`, `subject`, `serial_number`, `validity_not_before`, and `validity_not_after`, with text input fields in `scoConfig`. -- **Added**: `SHA-256` to `hashTypes` array, available for `x509-certificate` and `file` hash inputs. -- **Updated**: `generateBundle` to include new `x509-certificate` attributes in the SCO object within `observed-data.objects`. -- **Updated**: `handleBundleImport` to process new `x509-certificate` attributes as strings. -- **Meta**: Updated `` to `f7e5c3b4-0a6f-4d2b-8e1c-9b7a6d5e4f0a`. -- **Notes**: Preserved `goals` for `threat-actor`, relationship constraints, SCO nesting, and all other functionality. - -## [Stable] Version 92386760-6add-4294-bbdf-4b3b40b4fe62 - 2025-05-19 20:25 CEST -- **Fixed**: Corrected syntax error in `generateBundle` function, changing `if (key.includes('.' yok))` to `if (key.includes('.'))`, resolving the "Script error." runtime issue. -- **Meta**: Updated `` to `e6f4c2b3-9d5e-4c3a-8f1d-9a8b7c6d5e0f`. -- **Notes**: Marked as stable. Preserved all functionality, including `goals` for `threat-actor`, relationship constraints, and SCO nesting. - -## Version 7bda4770-1c15-4f92-9978-155f7e71fa11 - 2025-05-19 20:21 CEST -- **Added**: `goals` property to `threat-actor` SDO, with a text input for comma-separated values (e.g., `Financial Gain, Espionage`) in `sdoConfig`. -- **Updated**: `generateBundle` to process `goals` as a list of strings for `threat-actor`. -- **Updated**: `handleBundleImport` to join `goals` as a comma-separated string for `threat-actor` imports. -- **Meta**: Updated `` to `d5e3b1a2-8c4f-4d3b-9e2c-7f8a6b5c4d0e`. -- **Notes**: Ensured STIX 2.1 compliance for `goals` and preserved relationship constraints and SCO nesting. - -## Version 1301525f-336a-47eb-81a0-9f95f3a4756c - 2025-05-19 20:11 CEST -- **Added**: Relationship constraints based on provided JSON, restricting source SDOs (`indicator`, `malware`, etc.), target SDOs, and relationship types (`attributed-to`, `indicates`, `related-to`). -- **Updated**: Source Object `` to filter based on source’s `allowed_targets`. -- **Updated**: Relationship Type `` to include `observed-data` (removed exclusion filter). -- **Meta**: Updated `` to `b4e2c1f7-9a3d-4e8b-a2c9-fd7a6e5b3c4f`. -- **Notes**: Supported relationships like `indicator` → `indicates` → `observed-data` and maintained STIX 2.1 compliance. - -## Version da7f8258-086e-45fe-9d8a-e581563447a0 - 2025-05-16 15:22 CEST -- **Fixed**: Removed `observed-data` from relationship dropdowns (Source/Target Object ``. - - Enabled SDO-to-SDO relationship creation with basic validation. - - Supported bundle generation, download, and import with a modal interface. - - Used Tailwind CSS for styling and Babel for JSX transpilation. -- **Meta**: `` set to `380d42b4-f491-4f8b-b825-df384aafdc08`. -- **Notes**: Initial implementation lacked SCO nesting constraints and advanced relationship validation. Specific timestamp not provided in history. - ---- - -**Notes**: -- All versions maintain STIX 2.1 compliance, with progressive enhancements to enforce SCO nesting, relationship constraints, and property validation. -- Timestamps are based on the provided conversation history, with the earliest version lacking a specific time. -- The changelog assumes all listed `artifact_version_id` values from the history are included; if earlier versions exist, they were not provided. -- For further details on any version, refer to the corresponding `index.html` artifact. \ No newline at end of file diff --git a/CNAME b/CNAME deleted file mode 100644 index 9d78e48..0000000 --- a/CNAME +++ /dev/null @@ -1 +0,0 @@ -stix-generator.gurra.rocks \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index be80c0d..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 Gustav Alerby - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100644 index 92f8d01..0000000 --- a/README.md +++ /dev/null @@ -1,160 +0,0 @@ -# STIX Bundle Generator - -![STIX Bundle Generator Screenshot](screenshot.png) - -The STIX Bundle Generator is a web-based application for creating, importing, editing, and exporting STIX 2.1 bundles containing STIX Cyber-observable Objects (SCOs). Built with React and Tailwind CSS, it provides a user-friendly interface for cybersecurity professionals to manage threat intelligence data in the Structured Threat Information Expression (STIX) format. - -## Table of Contents -- [Purpose](#purpose) -- [Features](#features) -- [Installation](#installation) -- [Usage](#usage) -- [Contributing](#contributing) -- [License](#license) -- [Acknowledgments](#acknowledgments) - -## Purpose -The STIX Bundle Generator simplifies the process of working with STIX 2.1 bundles, enabling users to: -- Create SCOs (e.g., `email-addr`, `network-traffic`, `file`) with customizable fields. -- Import existing STIX bundles to view and edit SCOs. -- Export bundles for use in threat intelligence platforms. -- Visualize and manage SCOs through an intuitive interface. - -This tool is ideal for threat analysts, incident responders, and developers building STIX-compatible applications. - -## Features -- **Supported SCO Types**: 20 STIX 2.1 SCO types, including `artifact`, `autonomous-system`, `domain-name`, `email-addr`, `email-message`, `file`, `ipv4-addr`, `ipv6-addr`, `network-traffic`, `user-account`, `x509-certificate`, and more. -- **Interactive UI**: - - Clickable SCO chips for editing, with single-line inputs for primary and additional fields. - - Aggregated SCO display (e.g., `user-account (2)` for multiple `user-account` SCOs). - - Modal-based JSON import with a single "Import" button. -- **Bundle Management**: - - Generate STIX 2.1 bundles with unique IDs and timestamps. - - Download bundles as JSON files. - - Copy bundle JSON to clipboard. - - Clear all SCOs with a single button. -- **Customizable Fields**: - - Primary fields (e.g., `value` for `email-addr`, `protocols` for `network-traffic`) with single-value submission. - - Additional fields (e.g., `rir`, `is_multipart`, `src_ref`, `dst_ref`) with text, number, or checkbox inputs. - - Special handling for `x509-certificate` hashes (MD5, SHA-1, SHA-256, SHA-512). -- **Accessibility**: Non-selectable "Select SCO Type" placeholder and ARIA labels for buttons. -- **Styling**: Responsive design using Tailwind CSS. -- **Limitations** (current version): - - May produce duplicate `email-addr`, `ipv4-addr`, `ipv6-addr` SCOs during import. - - `from_ref`, `src_ref`, `dst_ref` display IDs (e.g., `email-addr--55555555-...`) instead of resolved values. - - Potential runtime errors (`openModal`, `downloadBundle`) due to direct `onClick` handlers. - -## Installation -The STIX Bundle Generator is a single-page web application that runs in the browser, requiring no server-side setup. - -### Prerequisites -- A modern web browser (e.g., Chrome, Firefox, Edge). -- No additional software or dependencies are required, as all libraries are loaded via CDN. - -### Steps -1. **Clone or Download**: - ```bash - git clone https://github.com/your-username/stix-bundle-generator.git - ``` - Alternatively, download the `index.html` file from the repository. - -2. **Open in Browser**: - - Open `index.html` directly in a web browser (e.g., double-click the file or use `file://` protocol). - - No local server is required, as the app uses CDN-hosted dependencies (React 18.2.0, ReactDOM 18.2.0, Babel Standalone 7.22.9, Tailwind CSS). - -3. **Optional: Serve Locally** (for development): - - Use a local server to avoid CORS issues with file-based access: - ```bash - npx http-server - ``` - - Navigate to `http://localhost:8080/index.html`. - -## Usage -1. **Launch the Application**: - - Open `index.html` in your browser. - - The interface displays a "Select SCO Type" dropdown and an "Import" button. - -2. **Create an SCO**: - - Select an SCO type (e.g., `user-account`). - - Enter the primary field value (e.g., `account_login: user1`) in the single-line input. - - Fill in additional fields (e.g., `user_id`, `account_type`) if applicable. - - Click "Add SCO" to add the SCO to "Added SCOs". - - Repeat to add multiple SCOs; chips show aggregated counts (e.g., `user-account (2)`). - -3. **Edit an SCO**: - - Click a chip in "Added SCOs" (e.g., `user-account (2)`) to edit the last SCO of that type. - - Update fields and click "Update SCO" or "Cancel". - -4. **Remove an SCO**: - - Click the `×` button on a chip to remove the last SCO of that type. - -5. **Import a Bundle**: - - Click "Import", paste a STIX 2.1 bundle JSON, and click "Submit". - - SCOs appear as chips in "Added SCOs" (note: duplicates may occur). - -6. **Generate and Export**: - - Click "Generate STIX Bundle" to create a bundle. - - Use "Download JSON" to save as `stix_bundle.json` or "Copy to Clipboard" to copy the JSON. - - Click "Clear All SCOs" to reset. - -### Example Bundle -```json -{ - "type": "bundle", - "id": "bundle--11111111-1111-1111-1111-111111111111", - "objects": [ - { - "type": "email-addr", - "id": "email-addr--55555555-aaaa-5555-aaaa-555555555555", - "spec_version": "2.1", - "value": "user@example.com" - }, - { - "type": "email-message", - "id": "email-message--66666666-aaaa-6666-aaaa-666666666666", - "spec_version": "2.1", - "is_multipart": false, - "date": "2023-01-01T12:34:56Z", - "from_ref": "email-addr--55555555-aaaa-5555-aaaa-555555555555", - "subject": "Test Email" - } - ] -} -``` - -## Contributing -Contributions are welcome! To contribute: -1. **Fork the Repository**: - ```bash - git fork https://github.com/your-username/stix-bundle-generator.git - ``` -2. **Create a Branch**: - ```bash - git checkout -b feature/your-feature - ``` -3. **Make Changes**: - - Address known issues (e.g., duplicate SCOs, unresolved `from_ref`/`src_ref`/`dst_ref`, runtime errors). - - Add features (e.g., email/IP validation, confirmation for "Clear All SCOs"). - - Update `CHANGELOG.md` with your changes. -4. **Test Locally**: - - Ensure `index.html` runs without errors. - - Test with sample bundles. -5. **Submit a Pull Request**: - - Push your branch and create a PR with a clear description of changes. - - Reference relevant issues or artifact versions. - -### Known Issues -- Duplicate `email-addr`, `ipv4-addr`, `ipv6-addr` SCOs during import. -- `from_ref`, `src_ref`, `dst_ref` display IDs instead of values. -- Potential runtime errors (`openModal`, `downloadBundle`) due to direct `onClick` handlers. - -## License -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. - -## Acknowledgments -- Created by Grok, narrated by gbyx3. -- Built with [React](https://reactjs.org/), [Tailwind CSS](https://tailwindcss.com/), and [Babel](https://babeljs.io/). -- Inspired by the need for accessible STIX 2.1 tools in cybersecurity. - ---- -*Last Updated: May 13, 2025* \ No newline at end of file diff --git a/bundles/stix_bundle_bundle--4ea55bac-1713-42d6-b837-636097e71d6d.json b/bundles/stix_bundle_bundle--4ea55bac-1713-42d6-b837-636097e71d6d.json new file mode 100644 index 0000000..ea6fcac --- /dev/null +++ b/bundles/stix_bundle_bundle--4ea55bac-1713-42d6-b837-636097e71d6d.json @@ -0,0 +1,32 @@ +{ + "type": "bundle", + "id": "bundle--4ea55bac-1713-42d6-b837-636097e71d6d", + "objects": [ + { + "type": "indicator", + "id": "indicator--7ffb8cdc-efa5-47b5-9a09-6be37a614fa0", + "spec_version": "2.1", + "created": "2025-06-14T12:24:06.780Z", + "modified": "2025-06-14T12:24:06.780Z", + "pattern": "IP = \"214.5.6.14\"", + "labels": [ + "C2" + ], + "valid_from": "2025-06-14T12:24:06.780Z", + "description": "Mythic" + }, + { + "type": "indicator", + "id": "indicator--8305e123-d956-4f7a-b82e-d3df4d759da9", + "spec_version": "2.1", + "created": "2025-06-14T12:25:05.324Z", + "modified": "2025-06-14T12:25:05.324Z", + "pattern": "IP = \"40.14.5.13\"", + "labels": [ + "C2" + ], + "valid_from": "2025-06-14T12:25:05.324Z", + "description": "Emotet" + } + ] +} \ No newline at end of file diff --git a/index.html b/index.html index ea2318e..d73fb22 100644 --- a/index.html +++ b/index.html @@ -1,1343 +1,872 @@ - - - - STIX Bundle Generator - - - - - - -
- + // Flush + function clearObjects() { + currentObjects = []; + currentRelationships = []; + currentBundle = null; + + updateObjectsList(); + updateRelationshipsList(); + updateObjectSelectors(); + updateBundleDisplay(); + + document.getElementById('validateBtn').disabled = true; + document.getElementById('saveBundleBtn').disabled = true; + document.getElementById('bundleValidation').innerHTML = ''; + + showMessage('All objects and relationships cleared'); + } + - + \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..ece404f --- /dev/null +++ b/main.py @@ -0,0 +1,297 @@ +from bottle import Bottle, run, static_file, request, response +import json +import os +from stixgenerator import STIXGenerator + +stix_generator = STIXGenerator() + +app = Bottle() + +session_objects = [] +session_relationships = [] + +@app.route('/static/') +def serve_static(filename): + return static_file(filename, root='./static') + +@app.route('/') +def index(): + return static_file('index.html', root='.') + +@app.route('/api/validate-bundle', method='POST') +def validate_bundle(): + try: + bundle_data = request.json + if not bundle_data: + response.status = 400 + return {'error': 'No bundle data provided'} + + validation_result = stix_generator.validate_bundle(bundle_data) + return validation_result + + except Exception as e: + response.status = 500 + return {'error': str(e)} + +# Endpoint to create STIX objects +@app.route('/api/create-object', method='POST') +def create_object(): + try: + data = request.json + object_type = data.get('type') + properties = data.get('properties', {}) + + if not object_type: + response.status = 400 + return {'error': 'Object type is required'} + + if object_type == 'indicator': + if 'pattern' not in properties or 'labels' not in properties: + response.status = 400 + return {'error': 'Indicator requires pattern and labels'} + stix_object = stix_generator.create_indicator( + properties['pattern'], + properties['labels'], + **{k: v for k, v in properties.items() if k not in ['pattern', 'labels']} + ) + elif object_type == 'malware': + if 'name' not in properties or 'labels' not in properties: + response.status = 400 + return {'error': 'Malware requires name and labels'} + stix_object = stix_generator.create_malware( + properties['name'], + properties['labels'], + **{k: v for k, v in properties.items() if k not in ['name', 'labels']} + ) + elif object_type == 'threat-actor': + if 'name' not in properties or 'labels' not in properties: + response.status = 400 + return {'error': 'Threat Actor requires name and labels'} + stix_object = stix_generator.create_threat_actor( + properties['name'], + properties['labels'], + **{k: v for k, v in properties.items() if k not in ['name', 'labels']} + ) + elif object_type == 'observed-data': + required_fields = ['number_observed', 'first_observed', 'last_observed', 'objects'] + if not all(field in properties for field in required_fields): + response.status = 400 + return {'error': f'Observed Data requires: {", ".join(required_fields)}'} + stix_object = stix_generator.create_observed_data( + properties['number_observed'], + properties['first_observed'], + properties['last_observed'], + properties['objects'] + ) + else: + stix_object = stix_generator.create_stix_object(object_type, properties) + + session_objects.append(stix_object) + + return stix_object + + except Exception as e: + response.status = 500 + return {'error': str(e)} + +# Endpoint to create relationships +@app.route('/api/create-relationship', method='POST') +def create_relationship(): + try: + data = request.json + source_ref = data.get('source_ref') + target_ref = data.get('target_ref') + relationship_type = data.get('relationship_type') + + if not all([source_ref, target_ref, relationship_type]): + response.status = 400 + return {'error': 'source_ref, target_ref, and relationship_type are required'} + + source_type = source_ref.split('--')[0] + target_type = target_ref.split('--')[0] + + if not stix_generator.validate_relationship(source_type, target_type, relationship_type): + response.status = 400 + return {'error': f'Invalid relationship: {source_type} -{relationship_type}-> {target_type}'} + + relationship = stix_generator.create_relationship(source_ref, target_ref, relationship_type) + session_relationships.append(relationship) + + return relationship + + except Exception as e: + response.status = 500 + return {'error': str(e)} + +# Endpoint to create bundles +@app.route('/api/create-bundle', method='POST') +def create_bundle(): + try: + data = request.json + objects = data.get('objects', []) + include_session_data = data.get('include_session_data', True) + all_objects = objects.copy() + + if include_session_data: + all_objects.extend(session_objects) + all_objects.extend(session_relationships) + + if not all_objects: + response.status = 400 + return {'error': 'No objects to include in bundle'} + + bundle = stix_generator.create_bundle(all_objects) + validation_result = stix_generator.validate_bundle(bundle) + + return { + 'bundle': bundle, + 'validation': validation_result, + 'total_objects': len(all_objects), + 'session_objects': len(session_objects), + 'session_relationships': len(session_relationships) + } + + except Exception as e: + response.status = 500 + return {'error': str(e)} + +# New endpoint to get current session data +@app.route('/api/session-data', method='GET') +def get_session_data(): + return { + 'objects': session_objects, + 'relationships': session_relationships, + 'total_objects': len(session_objects), + 'total_relationships': len(session_relationships) + } + +# New endpoint to clear session data +@app.route('/api/clear-session', method='POST') +def clear_session(): + global session_objects, session_relationships + session_objects = [] + session_relationships = [] + return {'message': 'Session data cleared'} + +# Endpoint to save the STIX bundle +@app.route('/api/save-bundle', method='POST') +def save_bundle(): + try: + bundle_data = request.json + if not bundle_data or bundle_data.get('type') != 'bundle': + response.status = 400 + return {'error': 'Invalid STIX bundle format'} + + validation_result = stix_generator.validate_bundle(bundle_data) + if not validation_result['valid']: + response.status = 400 + return { + 'error': 'Bundle validation failed', + 'validation_errors': validation_result['errors'] + } + + # Ensure bundles directory exists + if not os.path.exists('bundles'): + os.makedirs('bundles') + + filename = f"stix_bundle_{bundle_data.get('id', 'unknown').replace('bundle--', '')}.json" + bundle_path = os.path.join('bundles', filename) + + stix_generator.export_bundle_to_file(bundle_data, bundle_path) + + return { + 'success': True, + 'filename': filename, + 'validation': validation_result + } + + except Exception as e: + response.status = 500 + return {'error': str(e)} + +# Endpoint to load the STIX bundle +@app.route('/api/bundles') +def list_bundles(): + try: + bundles_dir = 'bundles' + if not os.path.exists(bundles_dir): + os.makedirs(bundles_dir) + return {'bundles': []} + + bundle_files = [f for f in os.listdir(bundles_dir) if f.endswith('.json')] + bundles = [] + + for filename in bundle_files: + try: + bundle_path = os.path.join(bundles_dir, filename) + bundle = stix_generator.import_bundle_from_file(bundle_path) + validation_result = stix_generator.validate_bundle(bundle) + + bundles.append({ + 'filename': filename, + 'id': bundle.get('id'), + 'objects_count': len(bundle.get('objects', [])), + 'created': os.path.getctime(bundle_path), + 'valid': validation_result['valid'], + 'validation_errors': validation_result.get('errors', []) + }) + except Exception as e: + bundles.append({ + 'filename': filename, + 'error': f'Failed to load: {str(e)}', + 'valid': False + }) + continue + + return {'bundles': bundles} + + except Exception as e: + response.status = 500 + return {'error': str(e)} + +# API endpoint to load a specific bundle +@app.route('/api/bundle/') +def get_bundle(filename): + try: + bundle_path = os.path.join('bundles', filename) + if not os.path.exists(bundle_path): + response.status = 404 + return {'error': 'Bundle not found'} + + bundle = stix_generator.import_bundle_from_file(bundle_path) + validation_result = stix_generator.validate_bundle(bundle) + + return { + 'bundle': bundle, + 'validation': validation_result + } + + except Exception as e: + response.status = 500 + return {'error': str(e)} + +# Get available STIX object types +@app.route('/api/object-types') +def get_object_types(): + return { + 'sdo_types': stix_generator.sdo_types, + 'sco_types': stix_generator.sco_types, + 'relationship_constraints': stix_generator.relationship_constraints + } + +# Enable CORS +@app.hook('after_request') +def enable_cors(): + response.headers['Access-Control-Allow-Origin'] = '*' + response.headers['Access-Control-Allow-Methods'] = 'PUT, GET, POST, DELETE, OPTIONS' + response.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token' + +@app.route('/', method='OPTIONS') +def handle_options(path): + return {} + +if __name__ == '__main__': + if not os.path.exists('bundles'): + os.makedirs('bundles') + # Debug is true for dev purposes + run(app, host='localhost', port=8080, debug=True, reloader=True) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..04016ec --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +bottle==0.12.25 \ No newline at end of file diff --git a/screenshot.png b/screenshot.png deleted file mode 100644 index 63e2ab8..0000000 Binary files a/screenshot.png and /dev/null differ diff --git a/stixgenerator.py b/stixgenerator.py new file mode 100644 index 0000000..b31419c --- /dev/null +++ b/stixgenerator.py @@ -0,0 +1,240 @@ +import json +import uuid +from datetime import datetime +from typing import Dict, List, Any, Optional + +class STIXGenerator: + """ + STIX Generator class + """ + def __init__(self): + self.sco_types = [ + 'artifact', 'autonomous-system', 'directory', 'domain-name', 'email-addr', + 'email-message', 'file', 'ipv4-addr', 'ipv6-addr', 'mac-addr', 'mutex', + 'network-traffic', 'process', 'software', 'url', 'user-account', + 'windows-registry-key', 'x509-certificate', 'crypto-currency-wallet', + 'crypto-currency-transaction' + ] + + self.sdo_types = [ + 'attack-pattern', 'campaign', 'course-of-action', 'grouping', 'identity', + 'indicator', 'infrastructure', 'intrusion-set', 'location', 'malware', + 'malware-analysis', 'note', 'observed-data', 'opinion', 'report', + 'threat-actor', 'tool', 'vulnerability' + ] + + self.relationship_constraints = { + "version": "2.1", + "relationships": { + "indicator": { + "allowed_targets": ["attack-pattern", "campaign", "intrusion-set", "malware", "tool", "threat-actor", "infrastructure", "vulnerability"], + "relationship_types": ["indicates", "related-to"] + }, + "malware": { + "allowed_targets": ["attack-pattern", "campaign", "intrusion-set", "indicator", "tool", "threat-actor", "infrastructure", "vulnerability"], + "relationship_types": ["uses", "targets", "related-to", "downloads", "drops"] + }, + "observed-data": { + "allowed_targets": ["indicator", "malware", "tool", "threat-actor", "attack-pattern", "campaign", "intrusion-set"], + "relationship_types": ["related-to"] + }, + "threat-actor": { + "allowed_targets": ["attack-pattern", "campaign", "intrusion-set", "indicator", "malware", "tool", "infrastructure", "vulnerability", "identity", "location"], + "relationship_types": ["uses", "targets", "attributed-to", "related-to", "impersonates", "located-at"] + }, + "attack-pattern": { + "allowed_targets": ["campaign", "intrusion-set", "indicator", "malware", "tool", "threat-actor", "vulnerability", "course-of-action"], + "relationship_types": ["uses", "targets", "related-to", "mitigated-by"] + }, + "campaign": { + "allowed_targets": ["attack-pattern", "intrusion-set", "indicator", "malware", "tool", "threat-actor", "infrastructure", "vulnerability", "identity"], + "relationship_types": ["uses", "targets", "attributed-to", "related-to"] + }, + "intrusion-set": { + "allowed_targets": ["attack-pattern", "campaign", "indicator", "malware", "tool", "threat-actor", "infrastructure", "vulnerability", "identity"], + "relationship_types": ["uses", "targets", "attributed-to", "related-to"] + }, + "tool": { + "allowed_targets": ["attack-pattern", "campaign", "intrusion-set", "indicator", "malware", "threat-actor", "infrastructure", "vulnerability"], + "relationship_types": ["uses", "targets", "related-to", "drops"] + }, + "infrastructure": { + "allowed_targets": ["attack-pattern", "campaign", "intrusion-set", "indicator", "malware", "tool", "threat-actor", "vulnerability"], + "relationship_types": ["uses", "hosts", "related-to", "communicates-with"] + }, + "vulnerability": { + "allowed_targets": ["attack-pattern", "campaign", "intrusion-set", "indicator", "malware", "tool", "threat-actor", "course-of-action"], + "relationship_types": ["related-to", "mitigated-by"] + }, + "course-of-action": { + "allowed_targets": ["attack-pattern", "vulnerability", "malware", "tool"], + "relationship_types": ["mitigates", "related-to"] + }, + "identity": { + "allowed_targets": ["attack-pattern", "campaign", "intrusion-set", "indicator", "malware", "tool", "threat-actor", "infrastructure", "vulnerability"], + "relationship_types": ["related-to", "targets"] + }, + "location": { + "allowed_targets": ["identity", "threat-actor", "campaign", "intrusion-set"], + "relationship_types": ["related-to", "located-at"] + } + } + } + + def generate_id(self, object_type: str) -> str: + return f"{object_type}--{str(uuid.uuid4())}" + + def get_current_timestamp(self) -> str: + return datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ') + + def create_stix_object(self, object_type: str, properties: Dict[str, Any]) -> Dict[str, Any]: + """ + Create a STIX object + """ + stix_object = { + "type": object_type, + "id": self.generate_id(object_type), + "spec_version": "2.1", + "created": self.get_current_timestamp(), + "modified": self.get_current_timestamp() + } + stix_object.update(properties) + + return stix_object + + def create_relationship(self, source_ref: str, target_ref: str, relationship_type: str) -> Dict[str, Any]: + """ + Create a STIX relationship object + """ + return self.create_stix_object("relationship", { + "source_ref": source_ref, + "target_ref": target_ref, + "relationship_type": relationship_type + }) + + def validate_relationship(self, source_type: str, target_type: str, relationship_type: str) -> bool: + """ + Validate if a relationship is allowed between two object types + """ + # If no constraints defined for source type, allow all relationships + constraints = self.relationship_constraints.get("relationships", {}) + source_constraints = constraints.get(source_type, {}) + + if not source_constraints: + return True + + allowed_targets = source_constraints.get("allowed_targets", []) + allowed_relationship_types = source_constraints.get("relationship_types", []) + target_allowed = not allowed_targets or target_type in allowed_targets + relationship_allowed = not allowed_relationship_types or relationship_type in allowed_relationship_types + + return target_allowed and relationship_allowed + + def create_bundle(self, objects: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + Create a STIX bundle containing multiple objects + """ + return { + "type": "bundle", + "id": self.generate_id("bundle"), + "objects": objects + } + + def validate_bundle(self, bundle: Dict[str, Any]) -> Dict[str, Any]: + """ + Validate a STIX bundle structure + """ + errors = [] + warnings = [] + + if bundle.get("type") != "bundle": + errors.append("Bundle must have type 'bundle'") + + if "objects" not in bundle or not isinstance(bundle["objects"], list): + errors.append("Bundle must contain an 'objects' array") + + if "objects" in bundle: + for obj in bundle["objects"]: + if obj.get("type") in self.sco_types: + errors.append(f"SCO '{obj.get('type')}' cannot be a top-level object") + + observed_data_objects = [obj for obj in bundle.get("objects", []) if obj.get("type") == "observed-data"] + + for obj in observed_data_objects: + if not obj.get("number_observed") or obj.get("number_observed") < 1: + errors.append(f"observed-data {obj.get('id')} must have number_observed >= 1") + + if not obj.get("first_observed") or not obj.get("last_observed"): + errors.append(f"observed-data {obj.get('id')} must have first_observed and last_observed") + + # Checking for nested SCOs + if "objects" in obj: + for sco_key, sco in obj["objects"].items(): + if sco.get("id") or sco.get("spec_version"): + errors.append(f"SCO in observed-data.objects must not include id or spec_version") + + return { + "valid": len(errors) == 0, + "errors": errors, + "warnings": warnings + } + + def create_indicator(self, pattern: str, labels: List[str], **kwargs) -> Dict[str, Any]: + """ + Create a STIX Indicator object + """ + properties = { + "pattern": pattern, + "labels": labels, + "valid_from": self.get_current_timestamp() + } + properties.update(kwargs) + return self.create_stix_object("indicator", properties) + + def create_malware(self, name: str, labels: List[str], **kwargs) -> Dict[str, Any]: + """ + Create a STIX Malware object + """ + properties = { + "name": name, + "labels": labels + } + properties.update(kwargs) + return self.create_stix_object("malware", properties) + + def create_threat_actor(self, name: str, labels: List[str], **kwargs) -> Dict[str, Any]: + """ + Create a STIX Threat Actor object + """ + properties = { + "name": name, + "labels": labels + } + properties.update(kwargs) + return self.create_stix_object("threat-actor", properties) + + def create_observed_data(self, number_observed: int, first_observed: str, last_observed: str, objects: Dict[str, Any]) -> Dict[str, Any]: + """ + Create a STIX Observed Data object + """ + properties = { + "number_observed": number_observed, + "first_observed": first_observed, + "last_observed": last_observed, + "objects": objects + } + return self.create_stix_object("observed-data", properties) + + def export_bundle_to_file(self, bundle: Dict[str, Any], filename: str) -> None: + """ + Export a STIX bundle to a JSON file + """ + with open(filename, 'w') as f: + json.dump(bundle, f, indent=2) + + def import_bundle_from_file(self, filename: str) -> Dict[str, Any]: + """ + Import a STIX bundle from a JSON file + """ + with open(filename, 'r') as f: + return json.load(f) \ No newline at end of file