November 5, 2026
How to Route Suspicious Documents to a Review Queue in Python
File scanners typically give us one of two outcomes: clean, or not clean. But in real document workflows, “not clean” is often too blunt. A…

By Cloudmersive
2 min read
File scanners typically give us one of two outcomes: clean, or not clean. But in real document workflows, "not clean" is often too blunt. A password-protected PDf or a macro-enabled spreadsheet will trip a strict scanner, yet both are routine business documents. Blocking them outright frustrates users; waving them through anyway defeats the purpose of the scan.
If we're stuck dealing with files like these in our workflow, the best bet is to send the ambiguous files to a human reviewer.
It's easy enough to automate that process. We'll build a three-way router in Python using an Enum for outcomes, a dataclass to carry the decision, and structural pattern matching (Python 3.10+) to dispath on it. As usual, we'll handle the actual scanning portion of our workflow with the Cloudmersive Advanced Scan API, whose per-category flags let us tell "dangerous" files apart from "work a second look" files.
Installing and configuring
Let's first install Cloudmersive:
pip install cloudmersive-virus-api-clientpip install cloudmersive-virus-api-clientAnd then let's configure our API key:
import os
import cloudmersive_virus_api_client
config = cloudmersive_virus_api_client.Configuration()
config.api_key["Apikey"] = os.environ["CLOUDMERSIVE_API_KEY"]
scan_api = cloudmersive_virus_api_client.ScanApi(
cloudmersive_virus_api_client.ApiClient(config)
)import os
import cloudmersive_virus_api_client
config = cloudmersive_virus_api_client.Configuration()
config.api_key["Apikey"] = os.environ["CLOUDMERSIVE_API_KEY"]
scan_api = cloudmersive_virus_api_client.ScanApi(
cloudmersive_virus_api_client.ApiClient(config)
)Modeling the decision
Now let's model three outcomes, and build a decision that carries the reasons behind it:
from enum import Enum
from dataclasses import dataclass, field
class Outcome(Enum):
CLEAN = "clean"
REVIEW = "review"
BLOCK = "block"
@dataclass
class Decision:
outcome: Outcome
reasons: list[str] = field(default_factory=list)from enum import Enum
from dataclasses import dataclass, field
class Outcome(Enum):
CLEAN = "clean"
REVIEW = "review"
BLOCK = "block"
@dataclass
class Decision:
outcome: Outcome
reasons: list[str] = field(default_factory=list)Triaging a scan result
The point is that some findings are unambiguously hostile, while other just warrant a closer look. We'll split the advanced scan's flags into those two groups and decide accordingly:
HARD_THREATS = {
"contains_executable": "executable content",
"contains_script": "embedded script",
"contains_insecure_deserialization": "insecure deserialization",
"contains_xml_external_entities": "XML external entity",
}
SOFT_FLAGS = {
"contains_macros": "macros",
"contains_password_protected_file": "password protection",
"contains_ole_embedded_object": "embedded OLE object",
}
def triage(result) -> Decision:
if result.found_viruses:
return Decision(Outcome.BLOCK, [v.virus_name for v in result.found_viruses])
hard = [label for attr, label in HARD_THREATS.items() if getattr(result, attr)]
if hard:
return Decision(Outcome.BLOCK, hard)
if result.clean_result is True:
return Decision(Outcome.CLEAN)
soft = [label for attr, label in SOFT_FLAGS.items() if getattr(result, attr)]
return Decision(Outcome.REVIEW, soft or ["flagged, reason unspecified"])HARD_THREATS = {
"contains_executable": "executable content",
"contains_script": "embedded script",
"contains_insecure_deserialization": "insecure deserialization",
"contains_xml_external_entities": "XML external entity",
}
SOFT_FLAGS = {
"contains_macros": "macros",
"contains_password_protected_file": "password protection",
"contains_ole_embedded_object": "embedded OLE object",
}
def triage(result) -> Decision:
if result.found_viruses:
return Decision(Outcome.BLOCK, [v.virus_name for v in result.found_viruses])
hard = [label for attr, label in HARD_THREATS.items() if getattr(result, attr)]
if hard:
return Decision(Outcome.BLOCK, hard)
if result.clean_result is True:
return Decision(Outcome.CLEAN)
soft = [label for attr, label in SOFT_FLAGS.items() if getattr(result, attr)]
return Decision(Outcome.REVIEW, soft or ["flagged, reason unspecified"])Routing the document
Now, the scan runs, triage decides, and match sends the file down the right path:
def route(path, review_queue: list[str]) -> Decision:
result = scan_api.scan_file_advanced(
path,
allow_executables=False,
allow_scripts=False,
allow_macros=False,
allow_invalid_files=False,
allow_password_protected_files=False,
allow_unsafe_archives=False,
)
decision = triage(result)
match decision.outcome:
case Outcome.CLEAN:
pass # hand off to normal processing
case Outcome.REVIEW:
review_queue.append(path) # a human looks before it proceeds
case Outcome.BLOCK:
pass # drop it; log decision.reasons
return decisiondef route(path, review_queue: list[str]) -> Decision:
result = scan_api.scan_file_advanced(
path,
allow_executables=False,
allow_scripts=False,
allow_macros=False,
allow_invalid_files=False,
allow_password_protected_files=False,
allow_unsafe_archives=False,
)
decision = triage(result)
match decision.outcome:
case Outcome.CLEAN:
pass # hand off to normal processing
case Outcome.REVIEW:
review_queue.append(path) # a human looks before it proceeds
case Outcome.BLOCK:
pass # drop it; log decision.reasons
return decisionNote thatreview_queue is a list here, but in a real-world context it's the seam where you'd push onto a SQS, a database table, or a Celery task in production.
Conclusion
Reading the individual scan flags instead of one clean/not-clean verdict lets you keep dangerous files out of your system while giving borderline ones the human judgement they actually need. The triage rules live in one place, so re-drawing the line between block and review is just a one-line change.