Skip to content

fix: [csv_loader] Validate type/category pairs and stop aborting whole import on one bad row - #9

Draft
elhoim wants to merge 1 commit into
MISP:mainfrom
elhoim:fix/csv-validation
Draft

elhoim wants to merge 1 commit into
MISP:mainfrom
elhoim:fix/csv-validation

Conversation

@elhoim

@elhoim elhoim commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

BLUF — CsvLoader::from_string now validates type/category pairs and skips bad rows instead of aborting.

  • Problem — CsvLoader::from_string in src/tools/csv_loader.rs validated type and category independently, never calling validate_type_category_pair, and every per-row error used ?, discarding already-parsed rows.
  • Fix — Calls validate_type_category_pair per row and collects row errors with continue plus log::warn!; it errors only when every row failed or the CSV structure is invalid.
  • Effect — Pairs like type=md5,category=Person are rejected client-side rather than by the server, and one malformed line no longer loses a good import. Two tests added.
  • CI — Red from a repo-level CI bug (ubuntu-22.04 runner lacks php8.3 mod_php; test_event_crud_lifecycle races the publish worker), fixed in unmerged PR fix: [ci] Run integration tests on ubuntu-24.04 to match php8.3 #53 and unrelated to this change.

Finding

type/category validated independently; pair validator never called, and one bad row aborts the import
validate_type_category_pair exists for exactly this, so type=md5,category=Person is accepted and rejected later by the server. Separately at :66 one malformed row discards every successfully parsed row.

Change

CsvLoader::from_string validated type and category independently and never called validate_type_category_pair, so type=md5,category=Person passed client-side validation despite the server rejecting that pair. Separately, every per-row error (parse error, type error, category error) used ?, aborting the whole import and discarding every already-parsed row. Fixed by calling validate_type_category_pair once per row (it subsumes the individual checks) and by collecting per-row errors with continue instead of propagating them via ?; bad rows are skipped and logged with log::warn!, good rows before/after are preserved. The public MispResult<Vec> signature is unchanged: an error is returned only when every attempted row failed or the CSV structure itself is invalid, so the two existing single-row-error tests (csv_invalid_type, csv_invalid_category) still pass unmodified.

Upstream evidence

MISP core app/Model/MispAttribute.php:997-998 checks in_array($fields['type'], $this->categoryDefinitions[$category]['types'], true) and rejects the attribute if the type is not listed for that category; categoryDefinitions is built from category_type_mappings-equivalent data at line ~4168-4170. This confirms a syntactically valid type + syntactically valid category can still be an invalid pair the server rejects, exactly the case validate_type_category_pair (src/validation.rs:103-119) exists to catch client-side. PyMISP's api.py has no bearing here since this is a purely client-side CSV convenience loader with no corresponding upstream PyMISP method to compare against.

Verified against MISP core 843b6744 and PyMISP e7debb4f.

Tests

  • csv_rejects_mismatched_type_category_pair
  • csv_skips_bad_rows_but_keeps_good_ones

Note on the original finding

Accurate on both counts. (1) validate_type_category_pair existed but was never called - confirmed by reading validation.rs and csv_loader.rs; type and category were checked with the standalone validate_type/validate_category functions only. (2) One malformed row did abort the whole import via ? inside the row loop, discarding all previously-parsed rows in that call - confirmed by reading the original loop body. No overstatement or understatement found; the fix matches the suggested approach (call the pair validator, collect per-row errors, preserve API shape).

Caveats

Did not add a test for the CSV-parse-error path at the original :66 map_err(...)? (now also collect-and-continue) - with flexible(true) and guaranteed-UTF-8 string input, that branch is effectively unreachable from from_string's public surface, so csv_skips_bad_rows_but_keeps_good_ones (which exercises the invalid-type per-row skip path) is the representative regression test for the "one bad row aborts everything" half of the finding. Both new tests were verified by construction to fail on the pre-fix code (mismatched pair was previously Ok(1); mixed valid/invalid rows previously returned Err) - could not run cargo test in this environment per the task's build/test restriction, so this is a by-inspection guarantee, not a captured red/green run.


Verified locally: cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, cargo test --all-features --lib, --test integration_tests --no-run, and cargo check --examples, each in an isolated target directory.

🤖 Generated with Claude Code

…e import on one bad row

CsvLoader::from_string validated `type` and `category` independently
(validation::validate_type / validate_category) but never called
validate_type_category_pair, so a syntactically valid but incompatible
pair like type=md5,category=Person was accepted client-side and would
only be rejected later by the MISP server. Confirmed against the pinned
MISP checkout: app/Model/MispAttribute.php checks
categoryDefinitions[$category]['types'] and rejects a type not listed
for that category (see the pair check around line 997-998, backed by
categoryDefinitions built at line ~4168).

Separately, every per-row validation error and the CSV parse error used
`?`, so one malformed or invalid row discarded every attribute already
parsed from earlier rows in the same call.

Fix: call validate_type_category_pair once per row (it also covers the
individual type/category checks), and collect per-row errors instead of
propagating them immediately. A row that fails is skipped and logged via
log::warn!; the good rows before and after it are preserved. The
existing MispResult<Vec<MispAttribute>> API shape is unchanged - an
error is returned only when every attempted row failed (aggregating all
row errors) or the CSV structure itself is invalid (e.g. missing
required columns), matching the two existing single-row error tests.

Added csv_rejects_mismatched_type_category_pair (fails before the fix:
md5/Person was previously accepted) and
csv_skips_bad_rows_but_keeps_good_ones (fails before the fix: the good
row was previously discarded along with the bad one).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JoSButbVaWEtGMXwTx73BZ
@elhoim
elhoim marked this pull request as draft September 3, 2026 05:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant