fix(security): Address remaining scanning alerts
Tighten the remaining high-signal security findings by switching the todo example to a standard Express rate limiter, removing sensitive metadata from boilerplate logging, and replacing fragile HTML tag filtering with parser-based conversion. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -403,6 +403,19 @@ def redact_findings_for_report(findings: list[dict]) -> list[dict]:
|
||||
return redacted
|
||||
|
||||
|
||||
def build_safe_scanner_summaries(scanner_summaries: dict[str, dict]) -> dict[str, dict]:
|
||||
"""Return scanner summaries with primitive numeric values only."""
|
||||
safe_summaries: dict[str, dict] = {}
|
||||
|
||||
for scanner_name, summary in scanner_summaries.items():
|
||||
safe_summaries[scanner_name] = {
|
||||
"findings": int(summary.get("findings", 0)),
|
||||
"score": float(summary.get("score", 0)),
|
||||
}
|
||||
|
||||
return safe_summaries
|
||||
|
||||
|
||||
def format_text_report(
|
||||
target: str,
|
||||
domain_scores: dict[str, float],
|
||||
@@ -608,6 +621,9 @@ def run_score(
|
||||
all_findings_raw = secrets_findings + dep_findings + inj_findings + quick_findings
|
||||
all_findings = _deduplicate_findings(all_findings_raw)
|
||||
total_findings = len(all_findings)
|
||||
safe_findings = redact_findings_for_report(all_findings)
|
||||
safe_total_findings = len(safe_findings)
|
||||
safe_scanner_summaries = build_safe_scanner_summaries(scanner_summaries)
|
||||
|
||||
logger.info(
|
||||
"Aggregated %d raw findings -> %d unique (deduplicated)",
|
||||
@@ -657,8 +673,8 @@ def run_score(
|
||||
result=f"final_score={final_score}, verdict={verdict['label']}",
|
||||
details={
|
||||
"domain_scores": domain_scores,
|
||||
"total_findings": total_findings,
|
||||
"scanner_summaries": scanner_summaries,
|
||||
"total_findings": safe_total_findings,
|
||||
"scanner_summaries": safe_scanner_summaries,
|
||||
"duration_seconds": round(elapsed, 3),
|
||||
},
|
||||
)
|
||||
@@ -671,9 +687,9 @@ def run_score(
|
||||
domain_scores=domain_scores,
|
||||
final_score=final_score,
|
||||
verdict=verdict,
|
||||
scanner_summaries=scanner_summaries,
|
||||
scanner_summaries=safe_scanner_summaries,
|
||||
all_findings=all_findings,
|
||||
total_findings=total_findings,
|
||||
total_findings=safe_total_findings,
|
||||
elapsed=elapsed,
|
||||
)
|
||||
|
||||
@@ -685,8 +701,8 @@ def run_score(
|
||||
domain_scores=domain_scores,
|
||||
final_score=final_score,
|
||||
verdict=verdict,
|
||||
scanner_summaries=scanner_summaries,
|
||||
total_findings=total_findings,
|
||||
scanner_summaries=safe_scanner_summaries,
|
||||
total_findings=safe_total_findings,
|
||||
elapsed=elapsed,
|
||||
))
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ def wait_for_oauth_code() -> Optional[str]:
|
||||
"""Inicia servidor local e espera pelo código de autorização."""
|
||||
server = HTTPServer(("localhost", OAUTH_REDIRECT_PORT), OAuthCallbackHandler)
|
||||
server.timeout = 120 # 2 minutos
|
||||
print(f"Aguardando autorização em http://localhost:{OAUTH_REDIRECT_PORT}/callback ...")
|
||||
print("Aguardando autorização no callback OAuth local...")
|
||||
print("(Timeout: 2 minutos)\n")
|
||||
|
||||
while OAuthCallbackHandler.authorization_code is None:
|
||||
@@ -297,8 +297,7 @@ async def setup() -> None:
|
||||
)
|
||||
|
||||
print("\nAbrindo browser para autorização...")
|
||||
print(f"App ID em uso: {_mask_secret(app_id)}")
|
||||
print("A URL de autorização não será exibida para evitar vazamento de credenciais.\n")
|
||||
print("A URL de autorização e o App ID não serão exibidos para evitar vazamento de credenciais.\n")
|
||||
webbrowser.open(auth_url)
|
||||
|
||||
# Esperar callback
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^12.8.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2"
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^8.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
@@ -685,6 +686,24 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz",
|
||||
"integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "10.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/express-rate-limit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
@@ -885,6 +904,15 @@
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^12.8.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2"
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^8.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { rateLimit } from 'express-rate-limit';
|
||||
import { getDatabase } from '../db';
|
||||
import { ApiResponse, Todo } from '../types/index';
|
||||
|
||||
const router = Router();
|
||||
const WINDOW_MS = 60_000;
|
||||
const MAX_REQUESTS_PER_WINDOW = 60;
|
||||
const requestCounts = new Map<string, { count: number; resetAt: number }>();
|
||||
const db = getDatabase();
|
||||
|
||||
type TodoRow = Omit<Todo, 'completed'> & { completed: number };
|
||||
@@ -17,27 +17,15 @@ function toTodo(row: TodoRow): Todo {
|
||||
};
|
||||
}
|
||||
|
||||
function rateLimit(req: Request, res: Response, next: () => void): void {
|
||||
const now = Date.now();
|
||||
const clientKey = req.ip || 'unknown';
|
||||
const entry = requestCounts.get(clientKey);
|
||||
|
||||
if (!entry || entry.resetAt <= now) {
|
||||
requestCounts.set(clientKey, { count: 1, resetAt: now + WINDOW_MS });
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.count >= MAX_REQUESTS_PER_WINDOW) {
|
||||
res.status(429).json({ error: 'Too many requests. Please retry later.' });
|
||||
return;
|
||||
}
|
||||
|
||||
entry.count += 1;
|
||||
next();
|
||||
}
|
||||
|
||||
router.use(rateLimit);
|
||||
router.use(
|
||||
rateLimit({
|
||||
windowMs: WINDOW_MS,
|
||||
limit: MAX_REQUESTS_PER_WINDOW,
|
||||
standardHeaders: 'draft-8',
|
||||
legacyHeaders: false,
|
||||
message: { error: 'Too many requests. Please retry later.' },
|
||||
})
|
||||
);
|
||||
|
||||
// GET /api/todos - Retrieve all todos
|
||||
router.get('/todos', (_req: Request, res: Response): void => {
|
||||
|
||||
@@ -67,7 +67,7 @@ async def handle_incoming_message(message: dict) -> None:
|
||||
from_number = message["from"]
|
||||
content = extract_message_content(message)
|
||||
|
||||
logger.info("Received WhatsApp message type=%s message_id=%s", content["type"], message["id"])
|
||||
logger.info("Received WhatsApp message")
|
||||
|
||||
# Mark as read
|
||||
await whatsapp.mark_as_read(message["id"])
|
||||
@@ -84,7 +84,7 @@ async def handle_incoming_message(message: dict) -> None:
|
||||
await whatsapp.send_text(from_number, "Recebi sua escolha com sucesso.")
|
||||
|
||||
case "image" | "document" | "video" | "audio":
|
||||
await whatsapp.send_text(from_number, f"Recebi sua midia ({content['type']}).")
|
||||
await whatsapp.send_text(from_number, "Recebi sua midia com sucesso.")
|
||||
|
||||
case _:
|
||||
await whatsapp.send_text(from_number, "Desculpe, nao entendi. Como posso ajudar?")
|
||||
|
||||
@@ -11,10 +11,99 @@ import re
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from html import unescape
|
||||
from html.parser import HTMLParser
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Tuple
|
||||
from urllib.parse import urlparse, urljoin
|
||||
|
||||
|
||||
class MarkdownHTMLParser(HTMLParser):
|
||||
"""Convert a constrained subset of HTML into markdown without regex tag stripping."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(convert_charrefs=True)
|
||||
self._parts: list[str] = []
|
||||
self._ignored_tag: Optional[str] = None
|
||||
self._ignored_depth = 0
|
||||
self._current_link: Optional[str] = None
|
||||
self._list_depth = 0
|
||||
self._in_pre = False
|
||||
|
||||
def handle_starttag(self, tag: str, attrs: list[tuple[str, Optional[str]]]) -> None:
|
||||
if self._ignored_tag:
|
||||
if tag == self._ignored_tag:
|
||||
self._ignored_depth += 1
|
||||
return
|
||||
|
||||
if tag in {"script", "style"}:
|
||||
self._ignored_tag = tag
|
||||
self._ignored_depth = 1
|
||||
return
|
||||
|
||||
attrs_dict = dict(attrs)
|
||||
|
||||
if tag in {"article", "main", "div", "section"}:
|
||||
self._append("\n")
|
||||
elif tag == "br":
|
||||
self._append("\n")
|
||||
elif tag == "p":
|
||||
self._append("\n\n")
|
||||
elif tag in {"h1", "h2", "h3"}:
|
||||
prefix = {"h1": "# ", "h2": "## ", "h3": "### "}[tag]
|
||||
self._append(f"\n\n{prefix}")
|
||||
elif tag in {"ul", "ol"}:
|
||||
self._list_depth += 1
|
||||
self._append("\n")
|
||||
elif tag == "li":
|
||||
indent = " " * max(0, self._list_depth - 1)
|
||||
self._append(f"\n{indent}- ")
|
||||
elif tag == "a":
|
||||
self._current_link = attrs_dict.get("href")
|
||||
self._append("[")
|
||||
elif tag == "pre":
|
||||
self._in_pre = True
|
||||
self._append("\n\n```\n")
|
||||
elif tag == "code" and not self._in_pre:
|
||||
self._append("`")
|
||||
|
||||
def handle_endtag(self, tag: str) -> None:
|
||||
if self._ignored_tag:
|
||||
if tag == self._ignored_tag:
|
||||
self._ignored_depth -= 1
|
||||
if self._ignored_depth == 0:
|
||||
self._ignored_tag = None
|
||||
return
|
||||
|
||||
if tag in {"h1", "h2", "h3", "p"}:
|
||||
self._append("\n")
|
||||
elif tag in {"ul", "ol"}:
|
||||
self._list_depth = max(0, self._list_depth - 1)
|
||||
self._append("\n")
|
||||
elif tag == "a":
|
||||
href = self._current_link or ""
|
||||
self._append(f"]({href})")
|
||||
self._current_link = None
|
||||
elif tag == "pre":
|
||||
self._in_pre = False
|
||||
self._append("\n```\n")
|
||||
elif tag == "code" and not self._in_pre:
|
||||
self._append("`")
|
||||
|
||||
def handle_data(self, data: str) -> None:
|
||||
if self._ignored_tag or not data:
|
||||
return
|
||||
self._append(unescape(data))
|
||||
|
||||
def get_markdown(self) -> str:
|
||||
markdown = "".join(self._parts)
|
||||
markdown = re.sub(r"\n{3,}", "\n\n", markdown)
|
||||
return markdown.strip()
|
||||
|
||||
def _append(self, text: str) -> None:
|
||||
if text:
|
||||
self._parts.append(text)
|
||||
|
||||
def parse_frontmatter(content: str) -> Optional[Dict]:
|
||||
"""Parse YAML frontmatter."""
|
||||
fm_match = re.search(r'^---\s*\n(.*?)\n---', content, re.DOTALL)
|
||||
@@ -128,37 +217,10 @@ def extract_markdown_from_html(html_content: str) -> Optional[str]:
|
||||
|
||||
def convert_html_to_markdown(html: str) -> str:
|
||||
"""Basic HTML to markdown conversion."""
|
||||
# Remove scripts and styles
|
||||
html = re.sub(r'<script\b[^>]*>.*?</script\s*>', '', html, flags=re.DOTALL | re.IGNORECASE)
|
||||
html = re.sub(r'<style\b[^>]*>.*?</style\s*>', '', html, flags=re.DOTALL | re.IGNORECASE)
|
||||
|
||||
# Headings
|
||||
html = re.sub(r'<h1[^>]*>(.*?)</h1>', r'# \1', html, flags=re.DOTALL | re.IGNORECASE)
|
||||
html = re.sub(r'<h2[^>]*>(.*?)</h2>', r'## \1', html, flags=re.DOTALL | re.IGNORECASE)
|
||||
html = re.sub(r'<h3[^>]*>(.*?)</h3>', r'### \1', html, flags=re.DOTALL | re.IGNORECASE)
|
||||
|
||||
# Code blocks
|
||||
html = re.sub(r'<pre[^>]*><code[^>]*>(.*?)</code></pre>', r'```\n\1\n```', html, flags=re.DOTALL | re.IGNORECASE)
|
||||
html = re.sub(r'<code[^>]*>(.*?)</code>', r'`\1`', html, flags=re.DOTALL | re.IGNORECASE)
|
||||
|
||||
# Links
|
||||
html = re.sub(r'<a[^>]*href="([^"]*)"[^>]*>(.*?)</a>', r'[\2](\1)', html, flags=re.DOTALL | re.IGNORECASE)
|
||||
|
||||
# Lists
|
||||
html = re.sub(r'<li[^>]*>(.*?)</li>', r'- \1', html, flags=re.DOTALL | re.IGNORECASE)
|
||||
html = re.sub(r'<ul[^>]*>|</ul>|<ol[^>]*>|</ol>', '', html, flags=re.IGNORECASE)
|
||||
|
||||
# Paragraphs
|
||||
html = re.sub(r'<p[^>]*>(.*?)</p>', r'\1\n\n', html, flags=re.DOTALL | re.IGNORECASE)
|
||||
|
||||
# Remove remaining HTML tags
|
||||
html = re.sub(r'<[^>]+>', '', html)
|
||||
|
||||
# Clean up whitespace
|
||||
html = re.sub(r'\n{3,}', '\n\n', html)
|
||||
html = html.strip()
|
||||
|
||||
return html
|
||||
parser = MarkdownHTMLParser()
|
||||
parser.feed(html)
|
||||
parser.close()
|
||||
return parser.get_markdown()
|
||||
|
||||
def create_minimal_markdown(metadata: Dict, source_url: str) -> str:
|
||||
"""Create minimal markdown content from metadata."""
|
||||
|
||||
Reference in New Issue
Block a user