feat(text): add text analysis pipeline & surface results in UI
- engine: add analyse_text() to extract visible page text and evaluate
category="text" rules; collect matched phrases and expose as
`content_snippet` (deduped, length-capped via settings.ui.snippet_preview_len).
- engine: removed unused code
- browser: removed double call for enrichment
- engine: improve regex compilation — honor per-rule flags (string or list)
and default IGNORECASE when category=="text".
- engine: add dispatch logging "[engine] applying categories: …" gated by
settings.app.print_rule_dispatch.
- ui(templates): add `templates/partials/result_text.html` mirroring the forms
table; renders page-level records and their matched rules.
- ui(controller): wire `analyse_text()` into scan path and expose
`payload["suspicious_text"]`.
- rules(text): add `identity_verification_prompt`, `gated_document_access`,
`email_collection_prompt`; broaden `credential_reset`.
fix: text indicators were not displayed due to missing analyzer and mismatched result shape.
Result shape:
suspicious_text: [
{
"type": "page",
"content_snippet": "...matched phrases…",
"rules": [
{"name": "...", "description": "...", "severity": "medium", "tags": ["..."]}
]
}
]
This commit is contained in:
@@ -54,5 +54,5 @@
|
||||
<p class="text-sm text-gray-500">No enrichment data available.</p>
|
||||
{% endif %}
|
||||
|
||||
<p class="mt-2"><a href="#top-jump-list" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
||||
<p class="mt-2"><a href="#url-overview" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
||||
</section>
|
||||
@@ -109,5 +109,5 @@
|
||||
<p class="text-sm text-gray-500">No form issues detected.</p>
|
||||
{% endif %}
|
||||
|
||||
<p class="mt-2"><a href="#top-jump-list" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
||||
<p class="mt-2"><a href="#url-overview" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
||||
</section>
|
||||
@@ -116,5 +116,5 @@
|
||||
<p class="text-sm text-gray-500">No suspicious scripts detected.</p>
|
||||
{% endif %}
|
||||
|
||||
<p class="mt-2"><a href="#top-jump-list" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
||||
<p class="mt-2"><a href="#url-overview" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
||||
</section>
|
||||
@@ -193,7 +193,7 @@
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
<p class="mt-2"><a href="#top-jump-list" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
||||
<p class="mt-2"><a href="#url-overview" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
||||
</div>
|
||||
</section>
|
||||
{% endmacro %}
|
||||
|
||||
120
app/templates/partials/result_text.html
Normal file
120
app/templates/partials/result_text.html
Normal file
@@ -0,0 +1,120 @@
|
||||
<!-- /templates/partials/result_text.html -->
|
||||
<section id="sus_text" class="card">
|
||||
<h2 class="text-lg font-semibold mb-3">Text</h2>
|
||||
|
||||
{% if suspicious_text and suspicious_text|length > 0 %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full table-fixed text-sm"> <!-- matches forms table style -->
|
||||
<colgroup>
|
||||
<col class="w-[10%]"> <!-- Source -->
|
||||
<col class="w-[10%]"> <!-- Indicators -->
|
||||
<col class="w-[15%]"> <!-- Tags -->
|
||||
<col class="w-[45%]"> <!-- Matches (Rules) -->
|
||||
<col class="w-[25%]"> <!-- Text Snippet -->
|
||||
</colgroup>
|
||||
<thead class="text-gray-400 border-b border-gray-800">
|
||||
<tr>
|
||||
<th class="text-left py-2 pr-4 whitespace-normal break-words">Source</th>
|
||||
<th class="text-left py-2 pr-4 whitespace-normal break-words">Indicators</th>
|
||||
<th class="text-left py-2 pr-4 whitespace-normal break-words">Tags</th>
|
||||
<th class="text-left py-2 pr-4 whitespace-normal break-words">Matches (Rules)</th>
|
||||
<th class="text-left py-2 pr-4 whitespace-normal break-words">Text Snippet</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for rec in suspicious_text %}
|
||||
<tr class="border-b border-gray-900 align-top">
|
||||
<!-- Source -->
|
||||
<td class="py-2 pr-4 break-words">
|
||||
{{ (rec.type or 'page')|title }}
|
||||
</td>
|
||||
|
||||
<!-- Indicators (count of rules matched) -->
|
||||
<td class="py-2 pr-4 whitespace-nowrap">
|
||||
{{ rec.rules|length if rec.rules else 0 }}
|
||||
</td>
|
||||
|
||||
<!-- Tags (unique across rules) -->
|
||||
<td class="py-2 pr-4 break-words">
|
||||
{% set ns = namespace(tags=[]) %}
|
||||
{% if rec.rules %}
|
||||
{% for r in rec.rules %}
|
||||
{% if r.tags %}
|
||||
{% for t in r.tags %}
|
||||
{% if t not in ns.tags %}
|
||||
{% set ns.tags = ns.tags + [t] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if ns.tags and ns.tags|length > 0 %}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{% for t in ns.tags %}
|
||||
<span class="chip" title="Tag: {{ t }}">{{ t }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="chip">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<!-- Matches (Rules) -->
|
||||
<td class="py-2 pr-4 break-words">
|
||||
{% if rec.rules and rec.rules|length > 0 %}
|
||||
<ul class="space-y-1">
|
||||
{% for r in rec.rules %}
|
||||
<li title="{{ r.description or '' }}">
|
||||
{{ r.name }}
|
||||
{% if r.severity %}
|
||||
{% set sev = r.severity|lower %}
|
||||
<span class="ml-2 rounded-full px-2 py-0.5 text-xs border
|
||||
{% if sev == 'high' %} badge badge-danger
|
||||
{% elif sev == 'medium' %} badge badge-warn
|
||||
{% else %} badge badge-info {% endif %}">
|
||||
{{ r.severity|title }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if r.tags %}
|
||||
{% for t in r.tags %}
|
||||
<span class="chip" title="Tag: {{ t }}">{{ t }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if r.description %}
|
||||
<small class="text-gray-400"> — {{ r.description }}</small>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<span class="text-gray-500">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<!-- Text Snippet (matched phrases; let column width control it) -->
|
||||
<td class="py-2 pr-4 align-top">
|
||||
{% if rec.content_snippet %}
|
||||
<details>
|
||||
<summary class="cursor-pointer text-blue-300 hover:underline">
|
||||
View snippet ({{ rec.content_snippet|length }} chars)
|
||||
</summary>
|
||||
<pre class="mt-1 bg-[#0b0f14] border border-gray-800 rounded-lg p-3
|
||||
w-full max-w-full overflow-auto max-h-64
|
||||
whitespace-pre-wrap break-words font-mono text-xs">{{ rec.content_snippet }}</pre>
|
||||
</details>
|
||||
{% else %}
|
||||
<span class="text-gray-500">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-500">No text issues detected.</p>
|
||||
{% endif %}
|
||||
|
||||
<p class="mt-2"><a href="#url-overview" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
||||
</section>
|
||||
Reference in New Issue
Block a user