id: tika-document-content-security-and-ingestion-gate
namespace: company.analytics
inputs:
- id: document_url
type: STRING
displayName: Document URL
description: Public or internal storage URL of the untrusted document to inspect
and sanitize.
defaults: "https://huggingface.co/datasets/kestra/datasets/resolve/main/pdf/app\
_store.pdf"
- id: declared_mime_type
type: SELECT
displayName: Declared MIME type
description: The MIME type declared by the client or file extension. Tika
verifies true content magic bytes against this value.
values:
- "application/pdf"
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
- "text/plain"
defaults: "application/pdf"
- id: min_characters_threshold
type: INT
displayName: Minimum extractable character threshold
description: Minimum number of extracted text characters required for multi-page
documents to pass without OCR escalation.
defaults: 100
- id: strict_mime_validation
type: BOOL
displayName: Enforce strict MIME matching
description: When true, any mismatch between declared MIME type and Tika
auto-detected MIME type triggers an immediate quarantine.
defaults: true
- id: quarantine_on_failure
type: BOOL
displayName: Quarantine on failure
description: When true, documents that fail security or quality checks fail the
execution gate and are barred from downstream ingestion.
defaults: true
- id: notify_slack
type: BOOL
displayName: Send Slack alert
description: Post security quarantine alerts to Slack. Requires the
SLACK_WEBHOOK_URL secret.
defaults: false
triggers:
- id: on_document_upload
type: io.kestra.plugin.core.trigger.Webhook
description: Real-time trigger invoked by upstream S3 bucket notification or
upload API when a new document lands.
key: "{{ secret('TIKA_INGESTION_WEBHOOK_KEY') }}"
- id: scheduled_batch_sweep
type: io.kestra.plugin.core.trigger.Schedule
description: Scheduled batch sweep to inspect newly staged documents.
cron: "0 4 * * *"
disabled: true
tasks:
- id: download_document
type: io.kestra.plugin.core.http.Download
description: Streams the untrusted document into Kestra internal storage.
uri: "{{ inputs.document_url }}"
- id: parse_content_and_metadata
type: io.kestra.plugin.tika.Parse
description: Extracts true MIME types, cryptographic encryption metadata, page
metrics, and body text using Apache Tika.
from: "{{ outputs.download_document.uri }}"
contentType: TEXT
store: false
- id: evaluate_content_security
type: io.kestra.plugin.scripts.python.Script
description: Inspects Tika metadata for MIME spoofing, password encryption
locks, ghost image scans, and metadata exposure.
containerImage: python:3.11-slim
beforeCommands:
- pip install kestra
env:
TIKA_METADATA: "{{ outputs.parse_content_and_metadata.result.metadata | toJson }}"
DECLARED_MIME: "{{ inputs.declared_mime_type }}"
MIN_CHARS: "{{ inputs.min_characters_threshold }}"
STRICT_MIME: "{{ inputs.strict_mime_validation }}"
CONTENT_LENGTH: "{{ outputs.parse_content_and_metadata.result.content ?
outputs.parse_content_and_metadata.result.content | length : 0 }}"
script: |
import os
import json
from kestra import Kestra
metadata_raw = os.environ.get("TIKA_METADATA", "{}")
try:
metadata = json.loads(metadata_raw)
except Exception:
metadata = {}
declared_mime = os.environ.get("DECLARED_MIME", "application/pdf").strip().lower()
min_chars = int(os.environ.get("MIN_CHARS", "100"))
strict_mime = os.environ.get("STRICT_MIME", "true").lower() == "true"
content_len = int(os.environ.get("CONTENT_LENGTH", "0"))
detected_mime = str(metadata.get("Content-Type", metadata.get("dc:format", "application/octet-stream"))).split(";")[0].strip().lower()
is_encrypted = False
for enc_key in ["pdf:encrypted", "isEncrypted", "hasOwnerPassword"]:
val = str(metadata.get(enc_key, "")).lower()
if val in ["true", "yes"]:
is_encrypted = True
break
page_count = 1
for page_key in ["xmpTPg:NPages", "Page-Count", "meta:page-count"]:
if page_key in metadata:
try:
page_count = int(metadata[page_key])
break
except (ValueError, TypeError):
pass
dangerous_mimes = [
"application/x-dosexec", "application/x-executable", "application/x-sharedlib",
"application/x-sh", "application/x-msdos-program", "application/x-msdownload",
"application/x-bat", "application/x-csh", "application/javascript"
]
verdict = "PASS_CLEAN"
details = []
if is_encrypted:
verdict = "QUARANTINE_ENCRYPTED"
details.append("Document is password protected or encrypted; automated RAG extraction blocked.")
elif detected_mime in dangerous_mimes:
verdict = "QUARANTINE_MIME_SPOOFED"
details.append(f"Security Alert: Executable or script payload ({detected_mime}) disguised as {declared_mime}.")
elif strict_mime and declared_mime and detected_mime != declared_mime:
verdict = "QUARANTINE_MIME_SPOOFED"
details.append(f"MIME Mismatch: Declared {declared_mime} but Tika auto-detected {detected_mime}.")
elif content_len < min_chars and page_count >= 1:
verdict = "ROUTE_OCR_REQUIRED"
details.append(f"Ghost Scanned Document: {page_count} page(s) detected but only {content_len} characters extracted. Secondary OCR required.")
else:
details.append(f"Clean document: {content_len} characters extracted across {page_count} page(s), verified MIME ({detected_mime}).")
print(f"Verdict: {verdict} - {'; '.join(details)}")
Kestra.outputs({
"verdict": verdict,
"detected_mime": detected_mime,
"page_count": page_count,
"content_length": content_len,
"is_encrypted": is_encrypted,
"summary": " ".join(details)
})
- id: route_security_decision
type: io.kestra.plugin.core.flow.Switch
description: Routes the document payload, quarantine records, or OCR dispatch
work orders based on the evaluated security verdict.
value: "{{ outputs.evaluate_content_security.vars.verdict }}"
cases:
PASS_CLEAN:
- id: stage_clean_payload
type: io.kestra.plugin.core.storage.Write
description: Persists clean extracted text payload to internal storage for
downstream RAG vector database ingestion.
extension: .txt
content: "{{ outputs.parse_content_and_metadata.result.content }}"
- id: log_clean_admission
type: io.kestra.plugin.core.log.Log
description: Logs admission verification metrics for the clean document.
message: "Document passed content security gate. Staged {{
outputs.evaluate_content_security.vars.content_length }} clean
characters across {{
outputs.evaluate_content_security.vars.page_count }} page(s) (MIME:
{{ outputs.evaluate_content_security.vars.detected_mime }})."
QUARANTINE_MIME_SPOOFED:
- id: log_mime_violation
type: io.kestra.plugin.core.log.Log
description: Logs high-severity security alert for MIME masquerading or script
payload injection.
message: "SECURITY ALERT: Quarantining document {{ inputs.document_url }}. True
MIME {{ outputs.evaluate_content_security.vars.detected_mime }}
violates declared {{ inputs.declared_mime_type }}."
- id: stage_spoofed_quarantine_manifest
type: io.kestra.plugin.core.storage.Write
description: Persists security quarantine manifest containing provenance, client
headers, and detected payload markers.
extension: .json
content: |
{
"verdict": "QUARANTINE_MIME_SPOOFED",
"executionId": "{{ execution.id }}",
"sourceUrl": "{{ inputs.document_url }}",
"declaredMime": "{{ inputs.declared_mime_type }}",
"detectedMime": "{{ outputs.evaluate_content_security.vars.detected_mime }}",
"summary": "{{ outputs.evaluate_content_security.vars.summary }}"
}
- id: check_slack_on_spoof
type: io.kestra.plugin.core.flow.If
description: Dispatches Slack security alert if Slack notification is enabled.
condition: "{{ inputs.notify_slack }}"
then:
- id: notify_security_violation
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Posts security alert to SecOps Slack channel.
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"text": "🚨 *Document Content Security Violation* | Execution `{{ execution.id }}`\n• *File:* `{{ inputs.document_url }}`\n• *Declared MIME:* `{{ inputs.declared_mime_type }}`\n• *Detected MIME:* `{{ outputs.evaluate_content_security.vars.detected_mime }}`\n• *Action:* Quarantined"
}
QUARANTINE_ENCRYPTED:
- id: log_encryption_hold
type: io.kestra.plugin.core.log.Log
description: Logs encryption hold requiring document password decryption.
message: "DOCUMENT HOLD: Document {{ inputs.document_url }} is encrypted or
password-protected. Quarantining to prevent RAG extraction failure."
- id: stage_encrypted_quarantine_manifest
type: io.kestra.plugin.core.storage.Write
description: Persists password retrieval manifest for client re-submission.
extension: .json
content: |
{
"verdict": "QUARANTINE_ENCRYPTED",
"executionId": "{{ execution.id }}",
"sourceUrl": "{{ inputs.document_url }}",
"isEncrypted": true,
"summary": "{{ outputs.evaluate_content_security.vars.summary }}"
}
ROUTE_OCR_REQUIRED:
- id: log_ocr_escalation
type: io.kestra.plugin.core.log.Log
description: Logs escalation to secondary OCR processing pool for image-only
scanned PDF.
message: "OCR ESCALATION: Document {{ inputs.document_url }} contains {{
outputs.evaluate_content_security.vars.page_count }} page(s) but
insufficient extracted text ({{
outputs.evaluate_content_security.vars.content_length }} chars).
Dispatching to OCR queue."
- id: stage_ocr_dispatch_work_order
type: io.kestra.plugin.core.storage.Write
description: Generates OCR processing work order manifest with storage pointer
and page counts.
extension: .json
content: |
{
"verdict": "ROUTE_OCR_REQUIRED",
"executionId": "{{ execution.id }}",
"sourceUrl": "{{ inputs.document_url }}",
"pageCount": {{ outputs.evaluate_content_security.vars.page_count }},
"extractedChars": {{ outputs.evaluate_content_security.vars.content_length }},
"internalStorageUri": "{{ outputs.download_document.uri }}"
}
- id: enforce_ingestion_gate
type: io.kestra.plugin.core.flow.If
description: Enforces pipeline gate, failing the execution if
quarantine_on_failure is enabled and the document was not clean.
condition: "{{ inputs.quarantine_on_failure and
outputs.evaluate_content_security.vars.verdict != 'PASS_CLEAN' }}"
then:
- id: log_gate_rejection
type: io.kestra.plugin.core.log.Log
description: Logs final policy rejection decision before stopping execution.
message: "Ingestion gate rejected document. Status: {{
outputs.evaluate_content_security.vars.verdict }}. Details: {{
outputs.evaluate_content_security.vars.summary }}"
- id: fail_execution
type: io.kestra.plugin.core.execution.Fail
description: Fails the execution so upstream orchestrators and dead-letter
monitoring catch the rejection.
errorMessage: "Document ingestion blocked by content security gate: {{
outputs.evaluate_content_security.vars.summary }}"