[backend/pycti] Prevent file upload looping (#14585)#14749
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #14749 +/- ##
==========================================
+ Coverage 32.35% 32.42% +0.06%
==========================================
Files 3107 3108 +1
Lines 211587 211869 +282
Branches 38361 38470 +109
==========================================
+ Hits 68469 68694 +225
- Misses 143118 143175 +57
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR addresses issue #14585 where playbook-driven “knowledge manipulation” updates on containers inadvertently delete/re-add existing attached files, causing noisy history entries and unnecessary file operations.
Changes:
- Backend: when the Container Wrapper component is configured to copy files, it now attempts to embed file content (
data) into the STIX file extensions instead of copying onlyurireferences. - PyCTI: removes the “fetch file by
uri” fallback during STIX import, so files are only uploaded whendatais explicitly embedded.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| opencti-platform/opencti-graphql/src/modules/playbook/playbook-components.ts | Updates container wrapping to resolve and embed file contents when copyFiles is enabled. |
| client-python/pycti/utils/opencti_stix2.py | Prevents implicit file re-download/re-upload by requiring embedded data for file import. |
opencti-platform/opencti-graphql/src/modules/playbook/playbook-components.ts
Outdated
Show resolved
Hide resolved
opencti-platform/opencti-graphql/src/modules/playbook/playbook-components.ts
Outdated
Show resolved
Hide resolved
| const currentFileUri = currentFile.uri; | ||
| const fileId = currentFileUri.replace('/storage/get/', ''); | ||
| const currentFileContent = toBase64(await getFileContent(fileId)); | ||
| copiedFiles.push({ ...currentFile, data: currentFileContent }); |
There was a problem hiding this comment.
Don't we need the file markings in addition of the file content ?
| const currentFileUri = currentFile.uri; | ||
| const fileId = currentFileUri.replace('/storage/get/', ''); | ||
| const currentFileContent = await getFileContent(fileId, 'base64'); | ||
| if (currentFileContent) { | ||
| copiedFiles.push({ ...currentFile, data: currentFileContent }); | ||
| } else { | ||
| logApp.error('Cant copy file from main element to the container: empty content', { name: currentFile.name }); | ||
| } | ||
| } catch (e) { | ||
| logApp.error('Cant copy file from main element to the container', { cause: e, name: currentFile.name }); | ||
| } | ||
| } | ||
| container.extensions[STIX_EXT_OCTI].files = copiedFiles; |
There was a problem hiding this comment.
When copying STIX file extensions and embedding data, we should also propagate/set no_trigger_import: true (same as convertStoreToStixWithResolvedFiles does) to avoid triggering automatic import/enrichment jobs for these re-uploaded attachments and potentially reintroducing loops.
| try { | ||
| const currentFileUri = currentFile.uri; |
There was a problem hiding this comment.
This assumes currentFile.uri is always a /storage/get/<id> path. In STIX 2.1 conversion, files that already include data often have uri: 'unknown'; in that case this code will try to fetch getFileContent('unknown', ...) and log errors. Consider short-circuiting when currentFile.data is already present (or when uri is missing/unknown) and just reuse the existing data.
| try { | |
| const currentFileUri = currentFile.uri; | |
| try { | |
| // If the file already contains data (common in STIX 2.1 bundles), reuse it directly. | |
| if (currentFile.data) { | |
| copiedFiles.push({ ...currentFile }); | |
| // eslint-disable-next-line no-continue | |
| continue; | |
| } | |
| const currentFileUri = currentFile.uri; | |
| // In some STIX 2.1 conversions, uri can be 'unknown' or missing; avoid bogus storage lookups. | |
| if (!currentFileUri || currentFileUri === 'unknown') { | |
| logApp.warn('Cant copy file from main element to the container: invalid or missing uri', { | |
| name: currentFile.name, | |
| uri: currentFileUri, | |
| }); | |
| // eslint-disable-next-line no-continue | |
| continue; | |
| } | |
| if (!currentFileUri.startsWith('/storage/get/')) { | |
| logApp.warn('Cant copy file from main element to the container: unexpected uri format', { | |
| name: currentFile.name, | |
| uri: currentFileUri, | |
| }); | |
| // eslint-disable-next-line no-continue | |
| continue; | |
| } |
opencti-platform/opencti-graphql/src/modules/playbook/playbook-components.ts
Outdated
Show resolved
Hide resolved
| const stixFileExtensions = baseData.extensions[STIX_EXT_OCTI].files; | ||
| if (copyFiles && stixFileExtensions && stixFileExtensions.length > 0) { | ||
| // We need to get the files and add the data inside | ||
| const copiedFiles = []; | ||
| for (let index = 0; index < stixFileExtensions.length; index += 1) { | ||
| const currentFile = stixFileExtensions[index]; | ||
| try { | ||
| const currentFileUri = currentFile.uri; | ||
| const fileId = currentFileUri.replace('/storage/get/', ''); | ||
| const currentFileContent = await getFileContent(fileId, 'base64'); | ||
| if (currentFileContent) { | ||
| copiedFiles.push({ ...currentFile, data: currentFileContent }); | ||
| } else { | ||
| logApp.error('Cant copy file from main element to the container: empty content', { name: currentFile.name }); | ||
| } | ||
| } catch (e) { | ||
| logApp.error('Cant copy file from main element to the container', { cause: e, name: currentFile.name }); | ||
| } | ||
| } | ||
| container.extensions[STIX_EXT_OCTI].files = copiedFiles; |
There was a problem hiding this comment.
The new copyFiles path now resolves each file's content from storage and injects it into the STIX file extension. There are existing integration tests for PLAYBOOK_CONTAINER_WRAPPER_COMPONENT, but none covering copyFiles: true; please add a test asserting that when copyFiles is enabled, container.extensions[STIX_EXT_OCTI].files includes data (and any required flags like no_trigger_import).
opencti-platform/opencti-graphql/src/modules/playbook/playbook-components.ts
Outdated
Show resolved
Hide resolved
| const currentFileUri = currentFile.uri; | ||
| const fileId = currentFileUri.replace('/storage/get/', ''); | ||
| const currentFileContent = await getFileContent(fileId, 'base64'); | ||
| if (currentFileContent) { |
There was a problem hiding this comment.
empty file (probably a usage error) should not trigger an error from copy logic perspective. we should log error only if file is not found ?
aHenryJard
left a comment
There was a problem hiding this comment.
Tested locally on report update with file attach and one that has a dedicated marking
=> use a playbook to copy report content into incident and it has worked as expected.
Tested also on report without files attached
1c346dd to
845696c
Compare
845696c to
19d8f1e
Compare
| if (currentFile.data) { | ||
| copiedFiles.push({ ...currentFile, no_trigger_import: true }); | ||
| } else { | ||
| // If data not in the element, fetch it in base64 | ||
| const currentFileUri = currentFile.uri; | ||
| const fileId = currentFileUri.replace('/storage/get/', ''); | ||
| const currentFileContent = await getFileContent(fileId, 'base64'); | ||
| if (currentFileContent) { // File does not exist anymore |
There was a problem hiding this comment.
The checks if (currentFile.data) and if (currentFileContent) rely on truthiness, so an empty (but valid) base64 string (e.g., a 0‑byte file) will be treated as “missing” and either re-fetched or skipped, which can lead to attachments being dropped from the container. Use explicit null/undefined checks instead (e.g., test for currentFile.data !== undefined and currentFileContent !== undefined).
| if (currentFile.data) { | |
| copiedFiles.push({ ...currentFile, no_trigger_import: true }); | |
| } else { | |
| // If data not in the element, fetch it in base64 | |
| const currentFileUri = currentFile.uri; | |
| const fileId = currentFileUri.replace('/storage/get/', ''); | |
| const currentFileContent = await getFileContent(fileId, 'base64'); | |
| if (currentFileContent) { // File does not exist anymore | |
| if (currentFile.data !== undefined && currentFile.data !== null) { | |
| copiedFiles.push({ ...currentFile, no_trigger_import: true }); | |
| } else { | |
| // If data not in the element, fetch it in base64 | |
| const currentFileUri = currentFile.uri; | |
| const fileId = currentFileUri.replace('/storage/get/', ''); | |
| const currentFileContent = await getFileContent(fileId, 'base64'); | |
| if (currentFileContent !== undefined && currentFileContent !== null) { // File does not exist anymore |
There was a problem hiding this comment.
@richard-julien same remark for me. The copilot suggestion is good
opencti-platform/opencti-graphql/src/modules/playbook/components/container-wrapper-component.ts
Outdated
Show resolved
Hide resolved
| if (currentFileContent) { // File does not exist anymore | ||
| copiedFiles.push({ ...currentFile, data: currentFileContent, no_trigger_import: true }); | ||
| } | ||
| } | ||
| } catch (e) { | ||
| logApp.error("Can't copy file from main element to the container", { cause: e, name: currentFile.name }); | ||
| } |
There was a problem hiding this comment.
Inline comment // File does not exist anymore is misleading here: getFileContent() can throw on missing S3 keys, and the if (currentFileContent) condition is about truthiness (also false for empty content), not existence. Please update the comment and/or handle the missing-file case explicitly (e.g., catching NoSuchKey and treating it as a non-error skip).
| if (currentFileContent) { // File does not exist anymore | |
| copiedFiles.push({ ...currentFile, data: currentFileContent, no_trigger_import: true }); | |
| } | |
| } | |
| } catch (e) { | |
| logApp.error("Can't copy file from main element to the container", { cause: e, name: currentFile.name }); | |
| } | |
| // Only copy the file if some (truthy) content was retrieved; missing-file errors are handled by the try/catch. | |
| if (currentFileContent) { | |
| copiedFiles.push({ ...currentFile, data: currentFileContent, no_trigger_import: true }); | |
| } | |
| } | |
| } catch (e) { | |
| logApp.error("Can't copy file from main element to the container", { cause: e, name: currentFile.name }); |
See #14585