Repository navigation
Send the two tables the agent collected and never shipped, and stop a NUL byte killing every port scan - #46
Merged
Conversation
… NUL byte killing every port scan Strip NUL from every bound parameter, store target_ip/state/banner, keep one bad row from ending a scan, add software_inventory and network_inventory to TABLES, bound the unsent backlog, and add the invariants that would have caught all of it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The agent was rebuilt and reinstalled to pick up the port scanner fix, and the
port table still had no new rows. Its newest was the previous day. The agent
log said why, ten times over:
The scan ran every hour, found its ports, and died on the write.
A NUL byte from a banner
PostgreSQL text cannot hold
\x00— the wire protocol terminates strings withit — so psycopg2 refuses the statement. And the scanner opens a socket to every
listening port and reads whatever comes back, which for a binary protocol is
binary.
decode(errors='ignore')does not save it: that drops bytes which arenot valid, and
\x00is a perfectly valid code point. The banner keeps it,the service and product strings are cut from the banner, and the insert dies.
Reproduced against the agent's own database:
Note the type:
ValueError, notpsycopg2.Error. Anexcept psycopg2.Errorwould have walked straight past it.
db.scrub_nulsnow strips NUL from every bound parameter — inserts, updatesand selects, because the scanner's duplicate check passes the same product
string to a SELECT and that raises first, so the row never reached the insert
that looked like the problem. Bytes are left alone;
byteastores NUL happilyand stripping there would corrupt the one case that was already correct.
Second fix, for the shape rather than the input: the insert loop sat inside the
try that reports the scan as failed, so the first unstorable row ended the scan
and took every port after it. One banner cost the whole hour. It is per-row now.
Three columns that were NULL on all 108 rows
target_ip,stateandbannerexist on the server'sportscan_resultanddid not exist on the agent's. The server's schema even says so:
-- Superset: product/version VEYA state/banner/target_ip ikisini de destekleNo agent version has ever sent the second half. So the console got a port list
that does not say which host or whether the port answered — a list of numbers.
Added to the agent schema and to the insert;
ADD COLUMN IF NOT EXISTSverified idempotent against the live agent database, which is what
apply_schemarequires since it runs the file on every start.Two tables collected every cycle and never sent
While checking the local database against the server's:
modules/inventory.pyhas been writing both since it was written —Inventory: Scanned 326 apps and 68 open portsis in the log every twenty minutes. Neithername was on
TABLESin the agent's main.py, so not one row was ever offered.The server was ready the entire time: both are in its
ALLOWED_TABLESand itsDEDUP_TABLES. It has been prepared to receive and deduplicate two tablesnothing ever sent it, and the console showed a machine with no software
installed and nothing listening.
Every layer was internally consistent, which is why it survived this long.
The backlog, and why it could grow
prune_sentrefuses to delete an unsent row. That is right for a table beingdelivered — deleting it is the data loss the module exists to prevent — and
exactly wrong for one that is not. So the two of them grew for the life of the
install, on an endpoint this product is supposed to be watching rather than
filling.
prune_unsentis the second bound, at 20,000 rows against the sent limit of5,000. Measured against the live database before committing to the number:
It bites only what is not being shipped. A healthy table drains fifty rows
every few seconds and never approaches it, and the log line for it is phrased
as a loss because it is one.
The invariants
None of this was caught by a test, so:
test_the_agent_ships_every_table_it_collects— every table in theagent's own schema is either on
TABLESor onAGENT_LOCAL_ONLYwith awritten reason. Two entries so far:
automationstravels the other way,security_audithas no collector. This is the test that fails today withoutthe fix.
test_nothing_is_excused_from_shipping_that_no_longer_exists— theexcuse list rots like any list.
test_an_unshipped_table_cannot_grow_for_ever— the bound stays wired in.test_a_nul_byte_does_not_reach_postgresandtest_every_bound_parameter_is_scrubbedacross all four helpers.test_the_scan_survives_a_row_it_cannot_store, and one per column fortarget_ip/state/bannerin both the insert and the agent schema.One more, small:
_schema_tableswas reading a table namedleavesout of thecomment
-- Existing agents: CREATE TABLE IF NOT EXISTS leaves the old CHECK in place.Harmless where the result was filtered againstALLOWED_TABLES, notharmless in a test that asks a question about every name it finds. Comments are
stripped first now, with the stripper the schema code already uses.
Testing
2533 passed, 5 skipped. The NUL failure and its fix were reproduced againstthe running agent database, and the schema migration applied and re-applied
there cleanly.
The agent needs rebuilding again for the code half to take effect; the schema
half applies itself on the next start.