Add iocs.files[] to database_specific for file indicators (per #1345 discussion) - #1394
Conversation
| #### iocs.files[].digests field | ||
|
|
||
| The `digests` field is an optional JSON object mapping a hash algorithm name to | ||
| the file's digest under that algorithm. Recognised algorithms are: |
There was a problem hiding this comment.
Maybe let's extend it to hashes recognized by VirustTotal (MD5, SHA1, SHA256, TLSH, SSDEEP) and add that it's prefered to add at least SHA256?
There was a problem hiding this comment.
Agreed. "md5", "sha1", "sha256", "tlsh", "ssdeep" should all be acceptable. "sha256" is preferred and recommended at a minimum.
|
|
||
| - `package-archive` — the file is part of the published package artifact. | ||
| - `downloaded` — the file was retrieved at run time (e.g. from a C2). | ||
| - `generated` — the file was produced at run time from code in the archive or |
There was a problem hiding this comment.
Maybe dropped instead of the generated, and add memory as an option for memory-only artifacts? Though I'm not sure if we want to include memory artifacts under files
There was a problem hiding this comment.
I also wonder if it's better to keep the list closed or open. No hard opinion.
There was a problem hiding this comment.
I'd prefer to not have unconstrained entries, or at least have a short conversation if a new value was desired.
As for "dropped" instead of "generated" how would that differ with "downloaded", as these are technically dropped as well?
I'd also be okay with just having "package-archive", "dropped" and perhaps "in-memory" as well to keep it simpler.
There was a problem hiding this comment.
Yeah, my thoughts were also going in the similar direction, so I'd be for the simplified version with dropped and in-memory. Additional information could be hold in the note
|
|
||
| #### iocs.files[].path field | ||
|
|
||
| The `path` field is an optional string giving the file name as observed, either |
There was a problem hiding this comment.
I think I'd suggest renaming it as paths and typing to the list - the same file can exist in multiple places.
There was a problem hiding this comment.
This sounds reasonable.
| - `sha256` — a lowercase hex-encoded SHA-256 digest (64 hex characters). | ||
| - `tlsh` — a hex-encoded [TLSH](https://tlsh.org/) fuzzy hash (70 hex |
There was a problem hiding this comment.
I'd suggest accepting any valid form of the hash (lower/upper/mixedcase for all, prefixed or not for TLSH) but performing normalization in the CI.
There was a problem hiding this comment.
My preference is that they are all hex-encoded lowercase. Although I am okay if they are mixed or uppercase as that is an easy conversion.
The CI can transform them to lowercase hex as needed.
| Files are recorded separately from the network indicators above, and each entry | ||
| carries its own `source` so that consumers can distinguish package artifacts | ||
| (the published tarball/wheel, which may only ever be processed as a stream) from | ||
| files that are dropped or generated at run time (for example a second-stage | ||
| payload downloaded from a C2, or content decoded from data embedded in the | ||
| archive). Keeping these apart avoids mixing, e.g., ten package tarballs and two | ||
| dropped files into a single flat list. |
There was a problem hiding this comment.
I think this paragraph could be omited.
There was a problem hiding this comment.
The paragraph could definitely be improved, although I am still keen to keep package artifacts (i.e. the archives downloaded from registries) separate from other files associated with the malware.
There was a problem hiding this comment.
My idea was that this is explained later when describing the source field - or did I not understand something?
calebbrown
left a comment
There was a problem hiding this comment.
It would be great to get your input here as well. Thanks!
| Files are recorded separately from the network indicators above, and each entry | ||
| carries its own `source` so that consumers can distinguish package artifacts | ||
| (the published tarball/wheel, which may only ever be processed as a stream) from | ||
| files that are dropped or generated at run time (for example a second-stage | ||
| payload downloaded from a C2, or content decoded from data embedded in the | ||
| archive). Keeping these apart avoids mixing, e.g., ten package tarballs and two | ||
| dropped files into a single flat list. |
There was a problem hiding this comment.
The paragraph could definitely be improved, although I am still keen to keep package artifacts (i.e. the archives downloaded from registries) separate from other files associated with the malware.
| "path": string, | ||
| "note": string, | ||
| "source": string, | ||
| "digests": { "sha256": string, "tlsh": string } |
There was a problem hiding this comment.
The digests object should be reformatted to be spread across multiple lines for legibility.
|
|
||
| - `package-archive` — the file is part of the published package artifact. | ||
| - `downloaded` — the file was retrieved at run time (e.g. from a C2). | ||
| - `generated` — the file was produced at run time from code in the archive or |
There was a problem hiding this comment.
I'd prefer to not have unconstrained entries, or at least have a short conversation if a new value was desired.
As for "dropped" instead of "generated" how would that differ with "downloaded", as these are technically dropped as well?
I'd also be okay with just having "package-archive", "dropped" and perhaps "in-memory" as well to keep it simpler.
…e digests) Per review from @kam193 and @calebbrown on ossf#1394: - path -> paths (a list; the same file can appear in several places) - simplify the source enum to package-archive | dropped | in-memory (downloaded/generated collapse into dropped; in-memory for memory-only artifacts) - digests: accept md5, sha1, sha256, tlsh, ssdeep (sha256 preferred/min recommended); hex digests validated case-insensitively and normalized to lowercase, ssdeep left as-is - multi-line the digests object in the overview; tighten the files[] intro paragraph - an entry must have at least one path or one digest Signed-off-by: Sascha Klein <akyroslabs@gmail.com>
|
Thanks @kam193 and @calebbrown — pushed a commit addressing all points:
Happy to keep iterating if anything else needs adjusting. |
|
One related thing while we finalize this: we also have several confirmed, novel malware reports open that all happen to carry a If they're waiting on the iocs schema landing here, that's completely understandable — but since they're confirmed malware, I'd rather the reports themselves not be blocked on the schema discussion. Happy to do whatever unblocks them on your end:
Just let me know what's easiest — mainly want to make sure they don't fall through the cracks. Thanks! |
|
@calebbrown loos good, hashes and path are paramount here. |
As for your other reports I am happy to leave them as just "hashes" for now, with a view to replace them with files later. |
|
I'm generally happy with this change, but I need to review the changes in more detail. I will have more comments on Monday. The code changes and overall spec are the most important to get right, we can always clean up the other wording later. I would also like a quick review from @awsactran too. |
|
Sorry for the late response as I am currently OOO. Overall I think this is a solid, well-scoped proposal. I do like that it stays inside database_specific rather than trying to alter OSV proper. |
calebbrown
left a comment
There was a problem hiding this comment.
Only some minor comments, but otherwise this looks good. Thankyou!
For reference I am intending within the next week or so to tighten the validation and make it possible to also mutate the DatabaseSpecific fields (this is to clean up CWEs, Indicators and other things).
Also, a structure for listing package artifacts themselves separately to IOCs will need proposed to support that usage.
| return fmt.Errorf("%w file note too long (%d > %d)", ErrUnexpectedOSV, len(f.Note), maxNoteLength) | ||
| } | ||
| d := f.Digests | ||
| hasDigest := d != nil && (d.MD5 != "" || d.SHA1 != "" || d.SHA256 != "" || d.TLSH != "" || d.SSDEEP != "") |
There was a problem hiding this comment.
I think the requirement should be for either MD5, SHA1 or SHA256 to be required. TLSH and SSDEEP are for content similarity checks, so they should not be one of the required.
| return nil | ||
| } | ||
| var err error | ||
| if d.MD5, err = normHex("md5", d.MD5, md5RE); err != nil { |
There was a problem hiding this comment.
It is worth noting that "normHex" will not be persisted at the moment. The indicators struct (inside DatabaseSpecific) is only currently used for validation.
That is "okay" at the moment, but the normalization can be done later. It may improve clarity to remove the mutation for now.
| } | ||
|
|
||
| // normHex validates a hex digest (case-insensitive) and returns it lowercased. | ||
| func normHex(name, val string, re *regexp.Regexp) (string, error) { |
There was a problem hiding this comment.
This is function is doing two things. My preference is for the validation to be independent of the ToLower() as it makes it simpler to read and understand.
| The `source` field is an optional string recording where the file came from. It | ||
| must be one of: | ||
|
|
||
| - `package-archive` — the file is part of the published package artifact. |
There was a problem hiding this comment.
This should probably be reworded as:
- the file is extracted from the published package artifact
This eliminates any confusion about it potentially including the artifact itself.
| Each entry carries a `source` so that package artifacts (the published | ||
| tarball/wheel, which may only ever be processed as a stream) are kept separate | ||
| from other files associated with the malware — a second stage dropped at run | ||
| time, or a payload that only ever lived in memory. |
There was a problem hiding this comment.
I think this paragraph could be improved. It should make it clearer what it is for, and be obvious that the package artifacts themselves do not belong in this set.
| - `package-archive` — the file is part of the published package artifact. | ||
| - `dropped` — the file was written to disk at run time (e.g. a second stage | ||
| retrieved from a C2, or content decoded from data embedded in the archive). | ||
| - `in-memory` — the file only ever existed in memory and never hit disk. |
There was a problem hiding this comment.
Can these enums be
- "PACKAGE_ARCHIVE"
- "DROPPED"
- "IN_MEMORY"
This keeps the enums consistent with the OSV enums, and allows the move to protojson based parsing of DatabaseSpecific data as well.
|
Thanks @calebbrown — addressed the review:
Makes sense re: tightening validation, mutable |
efcdb12 to
72c9a9e
Compare
Implements the structured file-IoC shape proposed in the review of ossf#1345: iocs.files[] records each file as {path, note, source, digests:{sha256,tlsh}}. The source enum (package-archive|downloaded|generated) keeps package artifacts separate from files dropped or generated at run time. path is optional when a file is identified only by a digest; each entry must carry a path or a digest. Adds the Indicators.Files field + validation (source enum, SHA-256/TLSH format, length limits), unit tests, and docs/schema_additions.md. Signed-off-by: Sascha Klein <akyroslabs@gmail.com>
…e digests) Per review from @kam193 and @calebbrown on ossf#1394: - path -> paths (a list; the same file can appear in several places) - simplify the source enum to package-archive | dropped | in-memory (downloaded/generated collapse into dropped; in-memory for memory-only artifacts) - digests: accept md5, sha1, sha256, tlsh, ssdeep (sha256 preferred/min recommended); hex digests validated case-insensitively and normalized to lowercase, ssdeep left as-is - multi-line the digests object in the overview; tighten the files[] intro paragraph - an entry must have at least one path or one digest Signed-off-by: Sascha Klein <akyroslabs@gmail.com>
…nestif) The expanded digest handling pushed Indicators.UnmarshalJSON over the gocyclo threshold (34 > 30) and tripped nestif. Extract validateFile / validateDigests / normHex so UnmarshalJSON stays simple; behaviour unchanged, tests still pass. Signed-off-by: Sascha Klein <akyroslabs@gmail.com>
… normalization; clarify docs - Rename file-source enums to OSV style: PACKAGE_ARCHIVE / DROPPED / IN_MEMORY - Decouple hex validation from lowercasing (normHex -> validateHex; normalize in validateDigests) - Docs: PACKAGE_ARCHIVE = "extracted from the published package artifact"; clarify the artifact itself is not part of this set Signed-off-by: Sascha Klein <akyroslabs@gmail.com>
72c9a9e to
72feaac
Compare
This implements the structured file-IoC shape @calebbrown proposed in the review of #1345, so that file indicators (hashes etc.) have a documented home — today
iocsonly definesdomains/ips/urls, and file hashes have nowhere schema-defined to go.iocs.files[]records each file as{ path, note, source, digests:{sha256,tlsh} }. Thesourceenum (package-archive/downloaded/generated) keeps package artifacts separate from files dropped or generated at run time, as suggested.pathis optional when a file is identified only by digest; each entry must carry apathor at least one digest.Includes the
Indicatorsstruct + validation (source enum, SHA-256/TLSH format, length limits), unit tests, anddocs/schema_additions.md.Flagged as a proposal — happy to adjust the shape (e.g. digest algorithms, required fields) per contributor consensus, as noted in #1345.