This case focused on identifying a shadow IT resource inside a GCP environment by comparing baseline inventory data against the current account state, then validating the anomaly through VPC Flow Logs.
Filed by Mycroft Holmes. Evidence sources: files/gcp_baseline_inventory.json, files/gcp_current_inventory.json, and gcp_flow_logs. Neither source alone told the full story — the inventory revealed the rogue instance, the flow logs revealed its behavior.
Baseline vs Current Inventory Comparison
A classic diff problem. jq extracted displayName values from each inventory file, both sets were sorted, and comm isolated entries that existed only in the current state:
jq -r '.[].displayName' files/gcp_baseline_inventory.json | sort > /tmp/base.txt
jq -r '.[].displayName' files/gcp_current_inventory.json | sort > /tmp/curr.txt
comm -13 /tmp/base.txt /tmp/curr.txt
Result: ctf-ssh-server-2
This established the initial anomaly — but not yet its network identity.
Asset Type Validation and Internal IP Resolution
A subtle complication: the same displayName appeared on more than one object type — one represented a disk, another the actual compute instance. The resource had to be filtered to the instance asset type before extracting networking data:
jq -r '.[]
| select(.assetType=="compute.googleapis.com/Instance" and .displayName=="ctf-ssh-server-2")
| .additionalAttributes.internalIPs[]' files/gcp_current_inventory.json
Internal IP: 10.0.1.5
Inventory analysis is not just about finding names — it's about finding the correct object type with the correct attributes. Moving too quickly from "match found" to "answer found" misses this.
Outbound TCP Flow Analysis
With the rogue VM's internal IP confirmed, the investigation moved into flow logs. The connection fields were nested, requiring structured field references:
SELECT
connection.dest_port,
COUNT(*) AS hits
FROM gcp_flow_logs
WHERE connection.src_ip = '10.0.1.5'
AND connection.protocol = 6
GROUP BY connection.dest_port
ORDER BY hits DESC;
Results showed common traffic on ports 443 and 80, plus one clearly unusual destination port: 1984. Not a routine service port, not in /etc/services.
Target Resource Identification
Isolated the destination the rogue instance was contacting on that port:
SELECT
connection.dest_ip,
COUNT(*) AS hits
FROM gcp_flow_logs
WHERE connection.src_ip = '10.0.1.5'
AND connection.protocol = 6
AND connection.dest_port = 1984
GROUP BY connection.dest_ip
ORDER BY hits DESC;
Destination IP: 3.15.208.127
Pivoted back into the inventory to resolve that address to a resource name:
jq -r '.[]
| select(.assetType=="compute.googleapis.com/Instance")
| .displayName + " " + (.additionalAttributes.externalIPs[]? // "")' files/gcp_current_inventory.json
ctf-web-server 104.197.194.68
ctf-ssh-server-2 34.46.93.202
Resource successfully discovered by the rogue VM: ctf-web-server
The Investigative Flow
Neither evidence source was used in isolation. The inventory files answered what resources existed, which object was new, and which attributes belonged to the true compute instance. The flow logs answered what the rogue resource was doing, which port was unusual, and which destination it contacted.
The sequence: prove something should not be there, then prove what it is doing. First structural anomaly, then behavioral confirmation. That sequence is clean, fast, and reusable across cloud environments.
Confirmed Findings
Rogue VM: ctf-ssh-server-2
Internal IP: 10.0.1.5
Unusual destination port: 1984
Destination IP: 3.15.208.127
Discovered resource: ctf-web-server