feat(ui): ataques (Evil Twin, regras/máscara, WPS), captura mais robusta

- app.js/app.css/index.html: telas novas para Evil Twin (PSK e
  Enterprise), regras de mutação/máscara na quebra de senha, WPS,
  indicador de modo (monitor/gerenciado) movido para baixo do seletor de
  interface, e aviso específico quando a placa não entrega captura de
  verdade (ver bug de ScanSnapshot.warning no engine).
- Lista de redes/BSSIDs não reordena mais a cada poll: ordem de chegada
  como critério de desempate e sinal arredondado em buckets de 4 dBm,
  porque reordenar pelo sinal instantâneo fazia as linhas flutuarem de
  posição debaixo do cursor a cada atualização.
- "Forçar handshake" religa a captura sozinho se ela tiver sido parada
  (esperando o AP reaparecer no canal certo antes de disparar o job, e
  travando o alvo numa variável local pra não perder o BSSID se o diálogo
  fechar no meio da espera).
- Grid da aba Ataques trocado de CSS grid pra colunas (alvenaria): cards
  com alturas muito diferentes na mesma linha deixavam buracos vazios
  enormes com align-items:start.
- check-ui-i18n.mjs: ajustes de cobertura para as novas telas.
This commit is contained in:
Lucas Rayan committed 2026-10-24 14:54:49 -03:00
1 parent a3ca73e610
commit 6e0cd7cf10
5 files changed
+3077 -76

No files matched your search

+285 -13
View File
@@ -68,6 +68,7 @@
:root[data-theme="dark"] { color-scheme: dark; }
* { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body { height: 100%; }
body {
margin: 0; background: var(--bg); color: var(--text);
@@ -104,17 +105,27 @@ code, .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
padding: 9px 10px; border-radius: 12px; transition: background .15s;
}
.dev:hover { background: color-mix(in srgb, var(--text) 6%, transparent); }
.dev[aria-current="true"] { background: var(--accent-soft); }
.dev[aria-current="true"], .dev[aria-pressed="true"] { background: var(--accent-soft); }
.dev .ico { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; background: var(--surface); box-shadow: 0 0 0 1px var(--line); color: var(--text-2); }
.dev[aria-current="true"] .ico { color: var(--accent); }
.dev[aria-current="true"] .ico, .dev[aria-pressed="true"] .ico { color: var(--accent); }
.dev .t { display: block; font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dev .s { display: block; font-size: 11.5px; color: var(--text-2); font-family: var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.side-foot { margin-top: auto; padding: 14px 10px 4px; font-size: 11.5px; color: var(--text-3); display: flex; justify-content: space-between; gap: 8px; }
.side-foot button { background: transparent; border: 0; color: var(--text-3); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
.iface-pick { display: flex; flex-direction: column; align-items: stretch; gap: 8px; padding: 0 10px 4px; }
.iface-pick .select { width: 100%; min-width: 0; }
.iface-pick .pill { align-self: flex-start; }
.side-foot { margin-top: auto; padding: 14px 4px 4px; font-size: 11.5px; color: var(--text-3); display: grid; gap: 8px; }
.foot-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.side-foot button { background: transparent; border: 0; color: var(--text-3); cursor: pointer; }
.side-foot .foot-row > button:not(.icon-btn) { text-decoration: underline; text-underline-offset: 2px; }
.side-foot button:hover { color: var(--text-2); }
.lang-sel { background: transparent; border: 0; color: var(--text-3); font-size: 11.5px; cursor: pointer; }
.lang-sel:hover { color: var(--text-2); }
.icon-btn { width: 30px; height: 30px; border-radius: 9px; display: inline-grid; place-items: center; padding: 0; }
.icon-btn:hover { background: color-mix(in srgb, var(--text) 8%, transparent); }
.theme-switch { display: inline-flex; padding: 2px; border-radius: 999px; background: color-mix(in srgb, var(--text) 8%, transparent); }
.theme-switch button { border: 0; background: transparent; cursor: pointer; padding: 4px 9px; border-radius: 999px; font-size: 11.5px; color: var(--text-2); }
.theme-switch button[aria-pressed="true"] { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0, 0, 0, .18); font-weight: 600; }
.main { overflow-y: auto; min-width: 0; }
.page { max-width: 1040px; margin: 0 auto; padding: 30px 36px 100px; animation: rise .35s cubic-bezier(.2, .8, .2, 1); }
@@ -133,6 +144,9 @@ code, .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
/* ------------------------------------------------------------ aviso */
.notice { display: flex; gap: 10px; background: var(--surface); border-radius: 12px; padding: 12px 14px; box-shadow: var(--shadow); font-size: 13px; color: var(--text-2); margin-bottom: 20px; align-items: flex-start; }
.notice svg { flex: none; margin-top: 2px; color: var(--warn); }
.notice.warn { background: var(--warn-soft); } .notice.warn svg { color: var(--warn); }
.notice.bad { background: var(--bad-soft); } .notice.bad svg { color: var(--bad); }
.notice b { color: var(--text); }
/* ------------------------------------------------------------ controles */
.controls { background: var(--surface); border-radius: var(--radius); box-shadow: var(--shadow); padding: 16px 18px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 22px; }
@@ -153,23 +167,57 @@ code, .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.btn.live:hover { filter: brightness(1.06); }
/* ------------------------------------------------------------ tabela */
.result-head { display: flex; align-items: baseline; justify-content: space-between; margin: 4px 2px 10px; }
.result-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin: 4px 2px 10px; }
.result-summary { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
.result-head strong { font-size: 15px; }
.result-head span { color: var(--text-3); font-size: 12.5px; }
.result-summary span { color: var(--text-3); font-size: 12.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.result-tools { display: flex; align-items: center; gap: 8px; }
.result-tools .filter { border: 0; border-radius: 10px; padding: 7px 12px; width: 210px; background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); font: inherit; font-size: 12.5px; }
.result-tools .filter::placeholder { color: var(--text-3); }
.result-tools .select { width: auto; padding: 7px 10px; border-radius: 10px; font-size: 12.5px; }
.ap-card { background: var(--surface); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; }
.net-card { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
thead th { text-align: left; padding: 11px 14px; font-size: 10.5px; color: var(--text-3); text-transform: uppercase; letter-spacing: .06em; font-weight: 600; border-bottom: 1px solid var(--line); }
tbody td { padding: 10px 14px; font-size: 13px; border-bottom: 1px solid var(--line); vertical-align: middle; }
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: var(--surface-2); }
td.bssid { font-family: var(--mono); font-size: 12px; color: var(--text-2); }
td.ssid { font-weight: 600; }
td.ssid em { color: var(--text-3); font-weight: 400; font-style: normal; }
.sec-tag { font-size: 11.5px; font-weight: 600; padding: 2px 8px; border-radius: 999px; }
.sec-aberta { background: var(--bad-soft); color: var(--bad); }
.sec-wep { background: var(--warn-soft); color: var(--warn); }
.sec-wpa, .sec-wpa2, .sec-wpa3 { background: var(--ok-soft); color: var(--ok); }
/* rede agrupada por ESSID; cada BSSID vira uma linha expansível */
.caret-cell { width: 34px; padding-left: 10px; padding-right: 0; }
.expander { width: 26px; height: 26px; border: 0; border-radius: 8px; background: transparent; color: var(--text-3); cursor: pointer; display: inline-grid; place-items: center; padding: 0; }
.expander:hover { background: color-mix(in srgb, var(--text) 8%, transparent); color: var(--text); }
.expander svg { transition: transform .18s; }
.net-row.open .expander svg { transform: rotate(90deg); }
.net-row.open > td { background: color-mix(in srgb, var(--text) 3%, transparent); }
.net-name .ssid { display: block; font-weight: 600; line-height: 1.25; }
.net-name .ssid em { color: var(--text-3); font-weight: 400; font-style: normal; }
.net-sub { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 3px; }
.flag { font-size: 10px; font-weight: 700; letter-spacing: .03em; padding: 1px 6px; border-radius: 999px; background: color-mix(in srgb, var(--text) 8%, transparent); color: var(--text-2); white-space: nowrap; }
.flag-warn { background: var(--warn-soft); color: var(--warn); }
.net-sec .sec-tag + .sec-tag { margin-left: 4px; }
.net-chan .chans { font-variant-numeric: tabular-nums; font-weight: 600; }
.net-band { display: block; font-size: 10.5px; color: var(--text-3); }
.net-signal, .net-clients { white-space: nowrap; font-variant-numeric: tabular-nums; }
.net-clients { color: var(--text-2); }
.detail-row td { padding: 4px 14px 10px 46px; background: var(--surface-2); }
.detail-row:hover { background: transparent; }
.bssid-list { display: grid; gap: 1px; }
.bssid-item { display: grid; grid-template-columns: minmax(150px, 1.4fr) 132px 112px minmax(92px, .8fr) 84px 1fr; gap: 10px; align-items: center; padding: 6px 8px; border-radius: 8px; font-size: 12.5px; }
.bssid-item:hover { background: color-mix(in srgb, var(--text) 4%, transparent); }
.bssid { color: var(--text-2); font-size: 11.5px; }
.bssid-meta, .bssid-clients { color: var(--text-3); font-size: 12px; }
.bssid-flags { display: flex; gap: 4px; justify-content: flex-end; flex-wrap: wrap; }
.sec-tag { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 999px; white-space: nowrap; }
.sec-open { background: var(--bad-soft); color: var(--bad); }
.sec-wep, .sec-wpa { background: var(--warn-soft); color: var(--warn); }
.sec-wpa2, .sec-wpa3, .sec-owe { background: var(--ok-soft); color: var(--ok); }
.sec-eap { background: var(--signal-soft); color: var(--signal); }
.signal-bars { display: inline-flex; align-items: flex-end; gap: 1.5px; height: 14px; vertical-align: middle; margin-right: 6px; }
.signal-bars i { width: 3px; background: var(--line); border-radius: 1px; display: block; }
.signal-bars i.on { background: var(--signal); }
@@ -179,6 +227,115 @@ td.ssid em { color: var(--text-3); font-weight: 400; font-style: normal; }
.empty-state h2 { font-size: 17px; color: var(--text); margin-bottom: 6px; }
.empty-state p { font-size: 13.5px; max-width: 360px; margin: 0 auto; }
/* ------------------------------------------------------------ configurações */
dialog { border: 0; border-radius: 20px; padding: 24px; width: min(420px, calc(100vw - 32px)); background: var(--surface); color: var(--text); box-shadow: 0 30px 80px rgba(0, 0, 0, .35); }
dialog::backdrop { background: rgba(0, 0, 0, .35); backdrop-filter: blur(6px); }
dialog h2 { font-size: 20px; margin-bottom: 16px; }
dialog menu { display: flex; gap: 10px; justify-content: flex-end; padding: 0; margin: 22px 0 0; }
.field { display: grid; gap: 6px; margin: 12px 0; }
.field-label { font-size: 12px; color: var(--text-2); font-weight: 600; }
.select { width: 100%; border: 0; border-radius: 12px; padding: 9px 14px; background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); font: inherit; cursor: pointer; }
.select option { background-color: var(--surface); color: var(--text); }
/* ------------------------------------------------------------ detalhes do alvo */
.ssid-btn { display: block; border: 0; padding: 0; background: transparent; color: inherit; font: inherit; font-weight: 600; text-align: left; cursor: pointer; line-height: 1.25; }
.ssid-btn:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
.bssid-item { cursor: pointer; }
.bssid-item:focus-visible { outline: 2px solid var(--signal); outline-offset: -2px; }
.target-dlg { width: min(980px, calc(100vw - 32px)); max-height: min(90vh, 900px); padding: 0; overflow: hidden; display: flex; flex-direction: column; }
.target-dlg:not([open]) { display: none; }
.target-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 22px 24px 12px; }
.target-head h2 { font-size: 22px; line-height: 1.2; }
.target-sub { display: flex; flex-wrap: wrap; align-items: center; gap: 5px 8px; color: var(--text-2); font-size: 12.5px; margin-top: 5px; }
.target-sub .mono { color: var(--text-2); font-size: 12px; }
.target-sub .sec-tag { margin-left: 2px; }
.dot-sep { color: var(--text-3); }
.bssid-select { width: auto; max-width: 320px; padding: 4px 8px; border-radius: 8px; font-size: 11.5px; }
.tabs { display: flex; gap: 2px; padding: 0 20px; border-bottom: 1px solid var(--line); }
.tabs button { border: 0; background: transparent; color: var(--text-2); padding: 9px 12px 11px; font-size: 13px; font-weight: 600; cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; }
.tabs button:hover { color: var(--text); }
.tabs button[aria-selected="true"] { color: var(--text); border-bottom-color: var(--accent); }
.tab-count { display: inline-block; min-width: 18px; padding: 0 5px; margin-left: 3px; border-radius: 999px; background: color-mix(in srgb, var(--text) 9%, transparent); color: var(--text-2); font-size: 10.5px; line-height: 17px; text-align: center; }
.tab-panel { padding: 18px 24px 26px; overflow-y: auto; min-height: 0; }
.detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.detail-card { background: var(--surface-2); border: 1px solid var(--line); border-radius: 14px; padding: 14px 16px; }
.detail-card h3 { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-3); margin-bottom: 10px; }
.detail-wide { margin-top: 12px; }
.kv { display: grid; grid-template-columns: minmax(96px, auto) minmax(0, 1fr); gap: 6px 12px; margin: 0; font-size: 12.5px; }
.kv dt { color: var(--text-3); }
.kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.detail-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.btn.small { padding: 6px 12px; font-size: 12px; }
.risk { border-radius: 12px; padding: 10px 13px; font-size: 12.5px; margin-bottom: 12px; border: 1px solid transparent; }
.risk-bad { background: var(--bad-soft); color: var(--bad); }
.risk-warn { background: var(--warn-soft); color: var(--warn); }
.risk-info { background: var(--signal-soft); color: var(--signal); }
.risk strong { font-weight: 700; }
.flag-ok { background: var(--ok-soft); color: var(--ok); }
.flag-bad { background: var(--bad-soft); color: var(--bad); }
.clients-card { margin-top: 2px; overflow-x: auto; }
.mini-table { width: 100%; border-collapse: collapse; }
.mini-table .row-actions { text-align: right; white-space: nowrap; }
.mini-table th { padding: 9px 12px; font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-3); text-align: left; border-bottom: 1px solid var(--line); }
.mini-table td { padding: 9px 12px; font-size: 12.5px; border-bottom: 1px solid var(--line); vertical-align: middle; }
.mini-table tr:last-child td { border-bottom: 0; }
.mini-table .nowrap { white-space: nowrap; font-variant-numeric: tabular-nums; }
.mini-table .probes { max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-2); }
.mini-table .device-cell { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-2); }
.clients-card .mini-table { table-layout: fixed; }
.clients-card .mini-table th:nth-child(1) { width: 20%; }
.clients-card .mini-table th:nth-child(2) { width: 18%; }
.clients-card .mini-table th:nth-child(3) { width: 12%; }
.clients-card .mini-table th:nth-child(4) { width: 7%; }
.clients-card .mini-table th:nth-child(5) { width: 10%; }
.clients-card .mini-table th:nth-child(6) { width: 12%; }
.clients-card .mini-table th:nth-child(7) { width: 21%; }
.clients-card .row-actions .btn { padding: 6px 10px; }
.clients-card .client-name, .clients-card .client-sub { max-width: 100%; }
.client-name { font-weight: 600; color: var(--text); max-width: 210px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.client-sub { color: var(--text-3); font-size: 10.5px; max-width: 210px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.empty-inline { text-align: center; padding: 46px 20px; color: var(--text-2); background: var(--surface-2); border-radius: 14px; }
.empty-inline strong { color: var(--text); display: block; margin-bottom: 4px; }
.empty-inline p { margin: 0; font-size: 13px; }
.auth-check { display: flex; align-items: center; gap: 8px; font-size: 12.5px; font-weight: 600; margin: 4px 0 14px; }
.auth-check input { width: 16px; height: 16px; accent-color: var(--accent); }
/* Colunas CSS em vez de grid: os cards de ataque têm alturas muito
diferentes entre si (Deauth/Handshake são enormes, PMF/WPS são curtos).
Num grid de 2 colunas comum, o par mais curto de uma linha deixava um
vão vazio enorme embaixo (a altura da linha é a do item mais alto, e
align-items:start não estica o curto pra preencher). Colunas fazem os
cards fluírem como alvenaria: cada um só ocupa a altura que precisa, o
próximo entra logo abaixo, sem buracos. */
.attack-grid { column-count: 2; column-gap: 12px; }
.attack-card { background: var(--surface-2); border: 1px solid var(--line); border-radius: 14px; padding: 14px 16px; min-width: 0; break-inside: avoid; margin: 0 0 12px; display: inline-block; width: 100%; }
.attack-deauth, .attack-wide { column-span: all; }
.attack-deauth .field-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.attack-deauth .detail-actions { align-items: center; }
.attack-card h3 { font-size: 14px; margin-bottom: 5px; }
.attack-card .hint { margin: 6px 0 10px; }
.attack-card .btn { margin-top: 2px; }
.field-row { display: grid; grid-template-columns: 1fr 1.4fr; gap: 10px; }
.field { display: grid; gap: 5px; margin: 9px 0; }
.field-label { font-size: 11.5px; color: var(--text-2); font-weight: 600; }
.input { width: 100%; border: 1px solid transparent; border-radius: 10px; padding: 8px 11px; background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); font: inherit; font-size: 12.5px; }
.input:focus { outline: 2px solid var(--signal); outline-offset: 1px; }
.hint { color: var(--text-3); font-size: 12px; margin: 0; line-height: 1.4; }
.hint.bad { color: var(--bad); }
.hint code { background: color-mix(in srgb, var(--text) 7%, transparent); padding: 1px 5px; border-radius: 5px; font-size: 11.5px; }
.hs-list { display: grid; gap: 6px; margin-bottom: 8px; }
.hs-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; background: var(--surface); border: 1px solid var(--line); border-radius: 10px; padding: 8px 10px; font-size: 12px; }
.hs-main { min-width: 0; display: grid; gap: 2px; }
.hs-meta { color: var(--text-3); font-size: 11px; }
.hs-flags { display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end; }
.crack-box { margin-top: 10px; padding: 9px 11px; border-radius: 10px; background: var(--surface); border: 1px solid var(--line); font-size: 12px; display: flex; flex-wrap: wrap; align-items: center; gap: 7px; }
.crack-ok { border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
.crack-box .mono { margin-right: 4px; }
.crack-head { display: flex; align-items: center; gap: 8px; width: 100%; color: var(--text-2); }
.crack-bar { width: 100%; height: 4px; border-radius: 999px; background: var(--line); overflow: hidden; }
.crack-bar i { display: block; width: 35%; height: 100%; border-radius: 999px; background: var(--accent); animation: crack-slide 1.1s ease-in-out infinite; }
@keyframes crack-slide { 0% { margin-left: -35%; } 100% { margin-left: 100%; } }
/* ------------------------------------------------------------ responsivo */
@media (max-width: 760px) {
.app { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
@@ -186,4 +343,119 @@ td.ssid em { color: var(--text-3); font-weight: 400; font-style: normal; }
.brand { padding: 4px 10px; }
.side-title, .side-foot { display: none; }
.list { display: flex; }
.result-tools { width: 100%; }
.result-tools .filter { flex: 1; width: auto; }
.net-table { min-width: 560px; }
.bssid-item { grid-template-columns: 1fr auto; }
.bssid-meta, .bssid-sig, .bssid-flags { grid-column: 1 / -1; }
.bssid-flags { justify-content: flex-start; }
.target-dlg { width: calc(100vw - 16px); max-height: 94vh; }
.target-head { padding: 16px 16px 10px; }
.tabs { padding: 0 10px; overflow-x: auto; }
.tab-panel { padding: 14px 14px 20px; }
.detail-grid { grid-template-columns: 1fr; }
.attack-grid { column-count: 1; }
.field-row { grid-template-columns: 1fr; }
.detail-card, .attack-card { padding: 12px; }
}
/* --------------------------------------------------- análise de canais */
.top-actions { display: flex; align-items: center; gap: 10px; }
.view-switch { display: inline-flex; padding: 2px; border-radius: 999px; background: color-mix(in srgb, var(--text) 7%, transparent); }
.view-switch button { border: 0; border-radius: 999px; padding: 5px 12px; font-size: 12px; font-weight: 600; color: var(--text-2); background: transparent; cursor: pointer; }
.view-switch button[aria-pressed="true"] { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0, 0, 0, .12); }
.alert-strip { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin: 0 2px 10px; }
.alert-item { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--line); background: var(--surface); border-radius: 999px; padding: 5px 11px; font-size: 12px; cursor: pointer; }
.alert-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--warn); }
.alert-high .alert-dot { background: var(--bad); }
.alert-low .alert-dot { background: var(--text-3); }
.alert-title { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.alert-more { color: var(--text-3); font-size: 11.5px; padding: 0 4px; }
.channel-card { padding: 16px; }
.channel-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
.band-switch { display: inline-flex; gap: 4px; }
.band-switch button { border: 1px solid transparent; background: color-mix(in srgb, var(--text) 6%, transparent); color: var(--text-2); border-radius: 999px; padding: 5px 12px; font-size: 12px; font-weight: 600; cursor: pointer; }
.band-switch button[aria-pressed="true"] { background: var(--signal-soft); color: var(--signal); border-color: color-mix(in srgb, var(--signal) 35%, transparent); }
.sp-legend { display: flex; flex-wrap: wrap; gap: 10px; color: var(--text-3); font-size: 11px; }
.sp-legend span { display: inline-flex; align-items: center; gap: 5px; }
.sp-legend i { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }
.spectrum { width: 100%; height: auto; display: block; background: color-mix(in srgb, var(--text) 3%, transparent); border-radius: 12px; }
.sp-grid { stroke: var(--line); stroke-width: 1; }
.sp-axis { stroke: color-mix(in srgb, var(--text) 22%, transparent); stroke-width: 1; }
.sp-db, .sp-chan { fill: var(--text-3); font-size: 9px; font-family: var(--mono); }
.sp-name { fill: var(--text-2); font-size: 9px; font-weight: 600; }
.sp-lobe path { transition: fill-opacity .15s; }
.sp-lobe:hover path { fill-opacity: .38; }
.channel-grid { display: grid; grid-template-columns: 190px 1fr; gap: 18px; margin-top: 14px; }
.channel-grid h3 { font-size: 12px; color: var(--text-2); margin-bottom: 8px; }
.chan-best-num { font-size: 44px; font-weight: 700; line-height: 1; color: var(--signal); margin: 0 0 6px; }
.chan-list { display: grid; gap: 6px; }
.chan-list h3 { margin: 0 0 2px; }
.chan-row { display: grid; grid-template-columns: 46px 1fr 48px 128px; align-items: center; gap: 8px; font-size: 12px; }
.chan-row .chan-n { font-weight: 600; display: inline-flex; align-items: center; gap: 4px; }
.chan-bar { height: 7px; border-radius: 999px; background: var(--line); overflow: hidden; }
.chan-bar i { display: block; height: 100%; border-radius: 999px; background: var(--ok); }
.chan-bar i.diff-mid { background: var(--warn); }
.chan-bar i.diff-high { background: var(--bad); }
.chan-score { font-family: var(--mono); font-size: 11px; color: var(--text-2); text-align: right; }
.chan-aps { color: var(--text-3); font-size: 11px; white-space: nowrap; }
.chan-best .chan-n, .chan-best .chan-aps { color: var(--signal); }
/* ------------------------------------------------- saúde do espectro (SMART) */
.chan-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 10px; margin: 2px 0 16px; }
.chan-tile { background: var(--surface-2); border-radius: 14px; padding: 12px 14px; display: flex; flex-direction: column; gap: 2px; box-shadow: 0 0 0 1px var(--line); }
.chan-tile small { color: var(--text-3); font-size: 12px; }
.chan-tile b { font-size: 18px; font-variant-numeric: tabular-nums; }
.chan-tile.status.ok { background: var(--ok-soft); } .chan-tile.status.ok b { color: var(--ok); }
.chan-tile.status.warn { background: var(--warn-soft); } .chan-tile.status.warn b { color: var(--warn); }
.chan-tile.status.bad { background: var(--bad-soft); } .chan-tile.status.bad b { color: var(--bad); }
.chan-tile b span { font-weight: 500; opacity: .8; font-size: 13px; margin-left: 4px; }
.chan-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--ok); margin-right: 5px; flex: none; }
.chan-dot.warn { background: var(--warn); }
.chan-dot.bad { background: var(--bad); }
.chan-attrs { margin-top: 4px; }
.chan-attrs summary { cursor: pointer; font-size: 12.5px; font-weight: 600; color: var(--text-2); padding: 6px 0; list-style: none; }
.chan-attrs summary::-webkit-details-marker { display: none; }
.chan-attrs summary::before { content: '▸ '; display: inline-block; width: 12px; }
.chan-attrs[open] summary::before { content: '▾ '; }
.chan-table .dim { color: var(--text-3); font-size: 11.5px; }
.chan-table tr.chan-best-row td { color: var(--signal); font-weight: 600; }
.mini-tag { font-size: 8.5px; padding: 1px 4px; border-radius: 4px; background: var(--warn-soft); color: var(--warn); }
/* ------------------------------------------------ clientes e linha do tempo */
.spark { display: inline-block; width: 62px; height: 16px; vertical-align: middle; margin-left: 6px; }
.spark path { fill: none; stroke: var(--signal); stroke-width: 1.4; }
.spark-big { display: block; width: 100%; height: 70px; margin: 10px 0 0; }
.timeline-card { margin-top: 12px; }
.timeline-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 6px; }
.timeline { list-style: none; padding: 0; margin: 8px 0; display: grid; gap: 5px; max-height: 260px; overflow: auto; }
.timeline li { display: grid; grid-template-columns: 84px 112px 1fr; align-items: baseline; gap: 8px; font-size: 12px; border-bottom: 1px solid var(--line); padding: 5px 2px; }
.timeline li > .mono:first-child { white-space: nowrap; }
.tl-kind { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; color: var(--text-3); }
.tl-deauth { color: var(--bad); }
.tl-associate { color: var(--ok); }
.tl-probe { color: var(--signal); }
.tl-detail { color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ------------------------------------------------------------- WPS e relatório */
.term { margin: 10px 0 0; padding: 10px 12px; border-radius: 10px; background: color-mix(in srgb, var(--text) 6%, transparent); color: var(--text-2); font: 11.5px/1.5 var(--mono); white-space: pre-wrap; max-height: 180px; overflow: auto; }
.report-dlg { width: min(1060px, 96vw); height: 92vh; max-height: 92vh; padding: 0; display: flex; flex-direction: column; overflow: hidden; }
.report-dlg:not([open]) { display: none; }
.report-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; padding: 16px 18px 12px; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
.report-head h2 { font-size: 17px; margin: 0 0 2px; }
.report-tools { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.report-tools .mono { font-size: 11.5px; color: var(--text-2); min-width: 38px; text-align: center; }
.report-view { flex: 1; overflow: auto; background: color-mix(in srgb, var(--text) 8%, var(--bg)); padding: 16px; }
.report-canvas { margin: 0 auto; transform-origin: top center; background: #fff; box-shadow: 0 6px 30px rgba(0, 0, 0, .18); }
.report-canvas iframe { border: 0; display: block; background: #fff; }
@media (max-width: 760px) {
.top-actions { gap: 6px; }
.channel-grid { grid-template-columns: 1fr; }
.chan-row { grid-template-columns: 42px 1fr 44px; }
.chan-aps { grid-column: 2 / -1; }
.report-dlg { width: 100vw; height: 100vh; max-height: 100vh; border-radius: 0; }
.report-view { padding: 8px; }
}
+2217 -48
View File
File diff suppressed because it is too large. Load diff
+468 -1
View File
@@ -2,8 +2,12 @@
export const EN = {
// index.html / sidebar
'auditoria de Wi-Fi': 'Wi-Fi auditing',
'Interface': 'Interface',
'Interface Wi-Fi': 'Wi-Fi interface',
'Interfaces': 'Interfaces',
'nenhuma interface Wi-Fi encontrada': 'no Wi-Fi interface found',
'APs, clientes e ataques': 'APs, clients and attacks',
'espectro e saúde': 'spectrum and health',
'checar atualização': 'check for update',
'checando…': 'checking…',
'atualizar para {n}': 'update to {n}',
@@ -30,7 +34,12 @@ export const EN = {
'parar captura': 'stop capture',
// resultados
'Redes': 'Networks',
'Redes vistas': 'Networks seen',
'Linha do tempo': 'Timeline',
'Linha do tempo ·': 'Timeline ·',
'fechar': 'close',
'motivo {n}': 'reason {n}',
'Pronto pra capturar': 'Ready to capture',
'Ative o modo monitor e inicie a captura pra ver as redes no ar.': 'Enable monitor mode and start capturing to see the networks on the air.',
'Nenhuma interface Wi-Fi': 'No Wi-Fi interface',
@@ -41,16 +50,44 @@ export const EN = {
'oculta': 'hidden',
'Rede': 'Network',
'Canal': 'Channel',
'Canais': 'Channels',
'Banda': 'Band',
'Bandas': 'Bands',
'Nome': 'Name',
'Sinal': 'Signal',
'Segurança': 'Security',
'Clientes': 'Clients',
'filtrar por SSID ou BSSID': 'filter by SSID or BSSID',
'Filtrar redes': 'Filter networks',
'Ordenar por': 'Sort by',
'expandir': 'expand',
'recolher': 'collapse',
'PMF parcial': 'partial PMF',
'canal {n}': 'channel {n}',
'canal {n} · {n}': 'channel {n} · {n}',
'1 cliente': '1 client',
'{n} clientes': '{n} clients',
'{n} de {n} redes': '{n} of {n} networks',
'{n} redes · {n} BSSIDs · {n} clientes': '{n} networks · {n} BSSIDs · {n} clients',
'Nenhuma rede corresponde ao filtro': 'No network matches the filter',
'Ajuste a busca ou limpe o campo de filtro.': 'Adjust the search or clear the filter.',
// erros de UI
'Não foi possível trocar o modo:': 'Could not change the mode:',
'Não foi possível iniciar a captura:': 'Could not start the capture:',
'Não foi possível retomar a captura: {n}': 'Could not resume the capture: {n}',
'Retomando a captura e localizando a rede…': 'Resuming the capture and locating the network…',
'A rede não reapareceu na varredura a tempo. Tente de novo.': 'The network did not reappear in the scan in time. Try again.',
'A captura parou:': 'The capture stopped:',
'erro desconhecido': 'unknown error',
'rota desconhecida': 'unknown route',
// seletor de idioma
// seletor de idioma / configurações
'Configurações': 'Settings',
'Tema': 'Theme',
'Claro': 'Light',
'Escuro': 'Dark',
'Fechar': 'Close',
'Idioma': 'Language',
'Auto': 'Auto',
'Português': 'Portuguese',
@@ -95,4 +132,434 @@ export const EN = {
'nenhum handshake quebrável para os filtros informados': 'no crackable handshake for the given filters',
'quantidade de quadros deve ficar entre 1 e 256': 'frame count must be between 1 and 256',
'MAC inválido: {n}': 'invalid MAC: {n}',
// detalhes do alvo e ataques
'não': 'no',
'não anunciado': 'not advertised',
'quebrável': 'crackable',
'Nenhum handshake capturado para este BSSID.': 'No handshake captured for this BSSID.',
'Rede aberta.': 'Open network.',
'O tráfego não é cifrado pelo Wi-Fi; qualquer cliente pode se associar.': 'Traffic is not encrypted over Wi-Fi; any client can associate.',
'A cifra é quebrável; capture IVs na aba Ataques.': 'The cipher is breakable; capture IVs in the Attacks tab.',
'O AP anuncia configuração travada.': 'The AP advertises configuration locked.',
'PIN pode estar acessível; verifique o lock.': 'The PIN may be accessible; check the lock.',
'Desautenticação e captura de handshake podem ser ignoradas pelos clientes.': 'Deauthentication and handshake capture may be ignored by clients.',
'Sem PMF obrigatório.': 'No mandatory PMF.',
'Desautenticação tende a funcionar e facilita a captura do handshake.': 'Deauthentication tends to work and makes handshake capture easier.',
'País': 'Country',
'Último visto': 'Last seen',
'Primeiro visto': 'First seen',
'Frequência': 'Frequency',
'Largura': 'Width',
'Padrão': 'Standard',
'Sinal máximo': 'Max signal',
'Intervalo de beacon': 'Beacon interval',
'Quadros de dados': 'Data frames',
'Versão WPS': 'WPS version',
'Vistos na rede': 'Seen on network',
'Quebráveis': 'Crackable',
'Número do modelo': 'Model number',
'Nome do dispositivo': 'Device name',
'Serial': 'Serial',
'Identificação': 'Identity',
'Rádio': 'Radio',
'sim': 'yes',
'Nome exibido': 'Display name',
'Hostname': 'Hostname',
'Sistema': 'OS',
'IP': 'IP',
'Associado a': 'Associated to',
'Atual': 'Current',
'Máximo': 'Maximum',
'Sem amostras de sinal suficientes ainda.': 'Not enough signal samples yet.',
'Linha do tempo · eventos mais recentes primeiro.': 'Timeline · most recent events first.',
'Cliente · {n}': 'Client · {n}',
'A captura parou sozinha:': 'The capture stopped on its own:',
'A placa não está capturando nada': 'The adapter is not capturing anything',
'Nenhum quadro recebido depois de alguns segundos saltando canais — esta placa pode aceitar ativar o modo monitor sem conseguir capturar de verdade (comum em Wi-Fi interna de notebook). Tente um adaptador USB dedicado a modo monitor/injeção.':
'No frame received after several seconds of channel hopping — this adapter may accept turning monitor mode on without actually being able to capture (common with a laptop\'s built-in Wi-Fi). Try a USB adapter dedicated to monitor mode/injection.',
'motivo desconhecido — confira se o adaptador ainda está conectado.':
'unknown reason — check whether the adapter is still connected.',
'Clientes e handshake': 'Clients and handshake',
'Nenhum cliente visto.': 'No client seen.',
'Associe ou gere tráfego na rede para que o Sabiá identifique estações.': 'Associate or generate traffic on the network so Sabiá can identify stations.',
'senha encontrada': 'password found',
'concluído': 'done',
'quebrando': 'cracking',
'senhas testadas': 'passwords tried',
'{n} senhas testadas': '{n} passwords tried',
'{n} tentativas · {n} ms': '{n} attempts · {n} ms',
'Senha não encontrada no dicionário ({n} tentativas).': 'Password not found in the wordlist ({n} attempts).',
'· quebrável': '· crackable',
'não especificado': 'unspecified',
'autenticação expirada': 'authentication expired',
'saindo': 'leaving',
'inativo': 'inactive',
'AP sem recursos': 'AP out of resources',
'classe 2': 'class 2',
'classe 3': 'class 3',
'{n} handshake(s) visto(s); {n} quebrável(is).': '{n} handshake(s) seen; {n} crackable.',
'Nenhum handshake capturado ainda. Desautentique um cliente para estimular a reassociação.': 'No handshake captured yet. Deauthenticate a client to trigger reassociation.',
'Captura WEP': 'WEP capture',
'WEP não tem handshake: o ataque coleta IVs em um .pcap. Reinicie a captura gravando o arquivo e gere tráfego com desautenticação. A captura atual é reiniciada e a tabela começa vazia.': 'WEP has no handshake: the attack collects IVs into a .pcap. Restart the capture writing the file and generate traffic with deauthentication. The current capture is restarted and the table starts empty.',
'Arquivo pcap': 'pcap file',
'Reiniciar captura com pcap': 'Restart capture to pcap',
'Gerar tráfego': 'Generate traffic',
'Copiar comando': 'Copy command',
'Esta rede não usa WEP.': 'This network does not use WEP.',
'Somente em redes próprias ou com autorização escrita.': 'Only on your own networks or with written authorization.',
'Cada ação fica registrada na trilha de auditoria local, com interface, BSSID e estação.': 'Every action is logged in the local audit trail with interface, BSSID and station.',
'Tenho autorização para testar esta rede': 'I have authorization to test this network',
'Desautenticação': 'Deauthentication',
'Expulsa uma estação (ou todas) para forçar reassociação e captura de handshake.': 'Kicks a station (or all) to force reassociation and handshake capture.',
'Enviar desautenticação': 'Send deauthentication',
'Forçar handshake': 'Force handshake',
'Dicionário': 'Wordlist',
'Rede oculta': 'Hidden network',
'job não encontrado': 'job not found',
'{n} quadro(s) enviados para {n} (motivo {n}).': '{n} frame(s) sent to {n} (reason {n}).',
'Selecione um handshake.': 'Select a handshake.',
'Informe o dicionário.': 'Enter the wordlist.',
'Informe o caminho do .pcap.': 'Enter the .pcap path.',
'Força bruta por máscara — sem dicionário, testa toda combinação do padrão.':
'Mask brute force — no wordlist, tests every combination of the pattern.',
'dígito,': 'digit,',
'minúscula,': 'lowercase,',
'maiúscula,': 'uppercase,',
'símbolo,': 'symbol,',
'qualquer um; o resto é literal.': 'any of those; the rest is literal.',
'Máscara': 'Mask',
'Força bruta por máscara': 'Mask brute force',
'Informe uma máscara (ex.: ?d?d?d?d?d?d?d?d).': 'Enter a mask (e.g. ?d?d?d?d?d?d?d?d).',
'job não iniciado': 'job not started',
'Captura gravando em {n}. Gere tráfego e aguarde IVs suficientes.': 'Capture writing to {n}. Generate traffic and wait for enough IVs.',
'Visão geral': 'Overview',
'Ver detalhes': 'View details',
'Ver clientes': 'View clients',
'Abrir ataques': 'Open attacks',
'Todos (broadcast)': 'All (broadcast)',
'associado': 'associated',
'sondando': 'probing',
'visto': 'seen',
'Alvo': 'Target',
'Handshake': 'Handshake',
'Salvar pcap em': 'Save pcap to',
'Exportar selecionado': 'Export selected',
'Exportar todos': 'Export all',
'Quebrar senha': 'Crack password',
'completo': 'complete',
'Ataques': 'Attacks',
'Cliente alvo': 'Target client',
'Ações': 'Actions',
'Atacar': 'Attack',
'Cliente não associado a este AP.': 'Client not associated with this AP.',
'Somente o cliente selecionado será desautenticado.': 'Only the selected client will be deauthenticated.',
'Todos os clientes associados (broadcast).': 'All associated clients (broadcast).',
'Fabricante': 'Manufacturer',
'Modelo': 'Model',
'Oculta': 'Hidden',
'Modo': 'Mode',
'Cifra': 'Cipher',
'Privacidade': 'Privacy',
'WPS travado': 'WPS locked',
'Associados': 'Associated',
'Senha': 'Password',
'Quadro': 'Frame',
'Quadros': 'Frames',
'ambas': 'both',
'Tentativas': 'Attempts',
'Testar PMF': 'Test PMF',
'em execução': 'running',
'iniciando': 'starting',
'erro': 'error',
'parado': 'stopped',
'Status: {n}': 'Status: {n}',
'tamanho inválido': 'invalid length',
'· última: {n}': '· last: {n}',
'Clona o AP (aberto) numa segunda interface e sobe um portal pedindo a senha de novo. A senha só é aceita se bater de verdade com o handshake já capturado — não é um "aceita qualquer coisa".':
'Clones the AP (open) on a second interface and shows a portal asking for the password again. The password is only accepted if it truly matches the already-captured handshake — it is not an "accept anything" page.',
'Exige um handshake quebrável já capturado deste AP (aba Handshake). Precisa de':
'Requires a crackable handshake already captured for this AP (Handshake tab). Needs',
'instalado e de uma': 'installed and a',
'interface Wi-Fi em modo AP, diferente da que está capturando.':
'Wi-Fi interface in AP mode, different from the one capturing.',
'Iniciar Evil Twin': 'Start Evil Twin',
'Informe a interface do clone (uma segunda placa).':
'Enter the clone interface (a second adapter).',
'a interface do clone precisa ser diferente da que está capturando':
'the clone interface must be different from the one capturing',
'esse BSSID ainda não apareceu nesta sessão de captura':
'that BSSID has not appeared in this capture session yet',
'rede oculta: sem SSID não dá pra clonar': 'hidden network: no SSID, cannot clone',
'handshake sem dados suficientes pra conferir senha':
'handshake does not have enough data to verify a password',
'hostapd não está instalado (sudo apt install hostapd) — necessário pro Evil Twin':
'hostapd is not installed (sudo apt install hostapd) — needed for Evil Twin',
'{n} · {n} credencial(is) capturada(s){n}': '{n} · {n} credential(s) captured{n}',
'Clona o AP numa segunda interface e deixa o hostapd-wpe aceitar a autenticação EAP de qualquer cliente. A credencial sai sozinha, como parte do próprio 802.1X — sem portal, sem ação humana.':
'Clones the AP on a second interface and lets hostapd-wpe accept the EAP authentication from any client. The credential comes out on its own, as part of 802.1X itself — no portal, no human action.',
'Rede Enterprise detectada (AKM 802.1X) — aqui a credencial é enviada automaticamente pelo cliente durante o handshake EAP, não precisa de portal. Exige':
'Enterprise network detected (AKM 802.1X) — here the credential is sent automatically by the client during the EAP handshake, no portal needed. Requires',
'instalado (não vem com o Sabiá: é um patch no build do hostapd —':
'installed (not bundled with Sabiá: it is a patch on the hostapd build —',
') e uma': ') and a',
'interface Wi-Fi em modo AP.': 'Wi-Fi interface in AP mode.',
'Iniciar Evil Twin Enterprise': 'Start Enterprise Evil Twin',
'Limitação honesta: o certificado TLS do clone é autoassinado. Dispositivo que valida a cadeia de certificado da rede (comum em perfil corporativo/MDM atualizado) vai recusar ou avisar o usuário — essa é a defesa que deveria existir. Funciona contra configuração sem essa validação.':
'Honest limitation: the clone\'s TLS certificate is self-signed. A device that validates the network\'s certificate chain (common on an updated corporate/MDM profile) will refuse or warn the user — that is the defense that is supposed to exist. It works against configurations without that validation.',
'essa rede não é WPA2/WPA3-Enterprise (802.1X) — pra rede com senha única (PSK), use o Evil Twin com portal cativo':
'this network is not WPA2/WPA3-Enterprise (802.1X) — for a single-password (PSK) network, use the Evil Twin with captive portal',
'hostapd-wpe não está instalado — não vem com o Sabiá (é um patch sobre o build do hostapd, não um binário pequeno); instale pelo repositório do Kali ou compile https://github.com/aircrack-ng/hostapd-wpe':
'hostapd-wpe is not installed — not bundled with Sabiá (it is a patch on the hostapd build, not a small binary); install it from the Kali repository or build https://github.com/aircrack-ng/hostapd-wpe',
'{n} está num modo que o hostapd-wpe não consegue usar':
'{n} is in a mode hostapd-wpe cannot use',
'não foi possível gerar o certificado TLS (openssl) pro hostapd-wpe':
'could not generate the TLS certificate (openssl) for hostapd-wpe',
'hostapd-wpe saiu sozinho ({n}) logo ao subir em {n} — a placa pode não suportar de verdade o canal/banda do AP real. Saída: {n}':
'hostapd-wpe exited on its own ({n}) right after starting on {n} — the adapter may not actually support the real AP\'s channel/band. Output: {n}',
'job de Evil Twin Enterprise não encontrado': 'Enterprise Evil Twin job not found',
'job de Evil Twin não encontrado': 'Evil Twin job not found',
'hostapd saiu sozinho ({n}) logo ao subir em {n} — a placa pode não suportar de verdade o canal/banda do AP real. Saída: {n}':
'hostapd exited on its own ({n}) right after starting on {n} — the adapter may not actually support the real AP\'s channel/band. Output: {n}',
'{n} está num modo que o hostapd não consegue usar': '{n} is in a mode hostapd cannot use',
'{n} handshake(s), {n} quadro(s) em {n}': '{n} handshake(s), {n} frame(s) in {n}',
'Motivo': 'Reason',
'Pacotes': 'Packets',
'Estado': 'State',
'Probes': 'Probes',
'Cliente': 'Client',
'WPS ativo': 'WPS active',
'WPS ativo.': 'WPS active.',
'WPS ativo ({n}).': 'WPS active ({n}).',
'PMF exigido.': 'PMF required.',
'Depois de coletar IVs suficientes:': 'After collecting enough IVs:',
'mensagens {n}': 'messages {n}',
'{n} · mensagens {n}': '{n} · messages {n}',
'senha: {n}': 'password: {n}',
'{n} · quebrável': '{n} · crackable',
'{n} · canal {n}': '{n} · channel {n}',
'{n} ({n})': '{n} ({n})',
// canais, PMKID/PMF, WPS e relatório
'Ocupação dos canais em {n}': 'Channel occupancy on {n}',
'Sem dados de canal ainda': 'No channel data yet',
'Inicie a captura para o Sabiá medir ocupação e sugerir canais.':
'Start capturing so Sabiá can measure occupancy and suggest channels.',
'{n} APs · {n} canais ocupados': '{n} APs · {n} busy channels',
'{n} AP(s) · recomendado': '{n} AP(s) · recommended',
'· recomendado': '· recommended',
'aberta/WEP': 'open/WEP',
'{n} rede(s) WPA3 analisada(s) — detalhes na aba do alvo e no relatório.':
'{n} WPA3 network(s) analyzed — details in the target tab and report.',
'Restrito a 1, 6 e 11 para evitar sobreposição.':
'Limited to 1, 6 and 11 to avoid overlap.',
'Menor pontuação de interferência entre os canais observados.':
'Lowest interference score among observed channels.',
'Melhor canal': 'Best channel',
'Espectro e canais': 'Spectrum and channels',
// saúde do espectro (canais, estilo SMART)
'Boa': 'Good',
'Atenção': 'Attention',
'Saturado': 'Saturated',
'Boa condição': 'Good condition',
'Sem canal livre nessa banda': 'No free channel on this band',
'Até o melhor canal está saturado': 'Even the best channel is saturated',
'Recomendado é DFS — pode cair com radar por perto': 'The recommended channel is DFS — it can drop off if radar is detected nearby',
'Espectro congestionado': 'Congested spectrum',
'Canal recomendado': 'Recommended channel',
'Canais ocupados': 'Busy channels',
'APs na banda': 'APs on this band',
'Pior canal': 'Worst channel',
'Canais DFS': 'DFS channels',
'Nessa banda, prefira sempre 1, 6 ou 11 — canais no meio sobrepõem vizinhos.':
'On this band, always prefer 1, 6 or 11 — channels in between overlap neighbors.',
'Canais DFS liberam mais espaço, mas o rádio é obrigado a sair do ar se detectar radar — tenha um canal não-DFS de reserva.':
'DFS channels free up more room, but the radio must go off the air if it detects radar — keep a non-DFS channel as backup.',
'Considere mudar o AP para o canal recomendado ou mover pra outra banda.':
'Consider moving the AP to the recommended channel or to another band.',
'Canais detalhados ({n})': 'Detailed channels ({n})',
'Pontuação': 'Score',
'APs': 'APs',
'{n} AP(s)': '{n} AP(s)',
'Recomendado': 'Recommended',
'Ainda sem eventos além do primeiro avistamento.':
'No events beyond the first sighting yet.',
'{n} amostra(s) de sinal. Eventos mais recentes primeiro.':
'{n} signal sample(s). Most recent events first.',
'nenhuma ferramenta instalada': 'no tool installed',
'Senha encontrada: {n}{n}': 'Password found: {n}{n}',
'Sem PIN, o reaver usa Pixie Dust; o bully usa o ataque padrão.':
'Without a PIN, reaver uses Pixie Dust; bully uses its default attack.',
'Instale reaver ou bully para habilitar. Ex.: sudo apt install reaver bully':
'Install reaver or bully to enable. E.g.: sudo apt install reaver bully',
'A saída do reaver/bully aparece aqui.': 'reaver/bully output appears here.',
'Orquestra reaver/bully na interface monitor. O canal do AP é usado automaticamente; o processo pode ser interrompido a qualquer momento.':
'Orchestrates reaver/bully on the monitor interface. The AP channel is used automatically; the process can be stopped at any time.',
'Ferramenta': 'Tool',
'PIN conhecido (opcional)': 'Known PIN (optional)',
'Exportar .hc22000': 'Export .hc22000',
'Todos .hc22000': 'Export all .hc22000',
'Iniciar WPS': 'Start WPS',
'Parar job': 'Stop job',
'8 dígitos': '8 digits',
'nenhum cliente visto': 'no client seen',
'Direção': 'Direction',
'AP → cliente': 'AP → client',
'cliente → AP': 'client → AP',
'Dispara deauth nas duas direções e observa se a estação reassocia. Resultado prático, não prova criptográfica.':
'Sends deauth in both directions and watches whether the station reassociates. Practical result, not cryptographic proof.',
'Estação': 'Station',
'Forja um MAC local e envia association request com PMKID para tentar a mensagem 1. Muitos APs só expõem PMKID em roaming real.':
'Forge a local MAC and send an association request with PMKID to try for message 1. Many APs only expose PMKID during real roaming.',
'Caçar PMKID': 'Hunt PMKID',
'Do cliente para o AP (exige estação selecionada).':
'From client to AP (requires a selected station).',
'Nas duas direções (exige estação selecionada).':
'In both directions (requires a selected station).',
'{n} quadro(s) {n} em {n} para {n} (motivo {n}).':
'{n} {n} frame(s) in {n} to {n} (reason {n}).',
'Aguardando saída do reaver/bully…': 'Waiting for reaver/bully output…',
'Enviando associação com PMKID…': 'Sending association with PMKID…',
'{n} quadro(s) enviados; PMKID não apareceu. {n}':
'{n} frame(s) sent; PMKID did not appear. {n}',
'Nenhuma estação disponível.': 'No station available.',
'Testando por ~4 s…': 'Testing for ~4 s…',
'PMKID capturado: {n}': 'PMKID captured: {n}',
'PMF não obrigatório': 'PMF not required',
'PMF não obrigatório · {n}': 'PMF not required · {n}',
'Inconclusivo · {n}': 'Inconclusive · {n}',
'Instale reaver ou bully.': 'Install reaver or bully.',
'Canal do AP desconhecido.': 'Unknown AP channel.',
'Montando o relatório…': 'Building the report…',
'não foi possível gerar o relatório': 'could not generate the report',
'Pré-visualização A4 · {n} página(s).': 'A4 preview · {n} page(s).',
'Pré-visualização A4.': 'A4 preview.',
'Paginando o relatório…': 'Paginating the report…',
'Não foi possível abrir a impressão; baixe o HTML e abra no navegador.':
'Could not open printing; download the HTML and open it in the browser.',
'não foi possível gerar o HTML': 'could not generate the HTML',
'Relatório': 'Report',
'Relatório de auditoria': 'Audit report',
'Imprimir / PDF': 'Print / PDF',
'Baixar HTML': 'Download HTML',
'Pré-visualização paginada em A4.': 'Paginated A4 preview.',
'Visualização': 'View',
'Relatório paginado': 'Paginated report',
'handshake sem SSID, PMKID ou mensagem 2 para exportar':
'handshake without SSID, PMKID or message 2 to export',
'nenhum handshake com material para .hc22000':
'no handshake with material for .hc22000',
'direção inválida: use ap, sta ou both': 'invalid direction: use ap, sta or both',
'máscara grande demais pra calcular o total': 'mask too large to calculate the total',
'máscara tem {n} combinações, acima do limite de {n} — restrinja a máscara (ex.: menos posições ?a/?l) antes de rodar':
'mask has {n} combinations, above the limit of {n} — narrow the mask (e.g. fewer ?a/?l positions) before running',
'quadro inválido: use deauth ou disassoc': 'invalid frame: use deauth or disassoc',
'direção sta/both exige a estação alvo': 'sta/both direction requires the target station',
'inicie a captura nesta interface antes do teste PMKID':
'start capturing on this interface before the PMKID test',
'BSSID ainda não visto nesta captura': 'BSSID not seen in this capture yet',
'SSID oculto: impossível montar o association request':
'hidden SSID: cannot build the association request',
'PMKID observado; exporte em .hc22000 e quebre com hashcat ou pelo Sabiá.':
'PMKID observed; export to .hc22000 and crack with hashcat or Sabiá.',
'Nenhum PMKID apareceu. O AP pode não usar cache de PMKID ou só responder a roaming real; tente também desautenticar um cliente associado.':
'No PMKID appeared. The AP may not cache PMKID or may only answer real roaming; also try deauthenticating an associated client.',
'PIN WPS deve ter 8 dígitos': 'WPS PIN must have 8 digits',
'a ferramenta terminou sem informar PSK': 'the tool finished without reporting a PSK',
'job WPS não encontrado': 'WPS job not found',
'Auditoria Wi-Fi · {n}': 'Wi-Fi audit · {n}',
'{n} está em modo managed; ative o modo monitor antes do teste PMKID':
'{n} is in managed mode; enable monitor mode before the PMKID test',
'{n} não encontrado no PATH; instale reaver ou bully':
'{n} not found in PATH; install reaver or bully',
'{n} está em modo managed; ative o modo monitor antes do ataque WPS':
'{n} is in managed mode; enable monitor mode before the WPS attack',
'não foi possível executar {n}: {n}': 'could not run {n}: {n}',
'{n} terminou com código {n}': '{n} exited with code {n}',
// achados do WIDS (analysis.rs)
'SSID “{n}” com seguranças diferentes': 'SSID “{n}” with different security levels',
'O mesmo nome aparece em BSSIDs com níveis de segurança distintos: {n}. Pode ser um evil twin ou um AP mal configurado.':
'The same name appears on BSSIDs with different security levels: {n}. It may be an evil twin or a misconfigured AP.',
'Rajada de desautenticação': 'Deauthentication burst',
'{n} quadros de desautenticação/desassociação no último minuto. Pode ser ataque de desautenticação ou um AP instável.':
'{n} deauthentication/disassociation frames in the last minute. It may be a deauth attack or an unstable AP.',
'Muitos BSSIDs novos': 'Many new BSSIDs',
'{n} BSSIDs apareceram no último minuto — possível beacon flood ou ambiente muito denso.':
'{n} BSSIDs appeared in the last minute — possible beacon flood or a very dense environment.',
'Rede aberta: {n}': 'Open network: {n}',
'Sem cifra no Wi-Fi; o tráfego fica exposto a qualquer estação no alcance.':
'No Wi-Fi encryption; traffic is exposed to any station in range.',
'WEP em {n}': 'WEP on {n}',
'WEP é quebrável em minutos; trate a rede como aberta.':
'WEP can be cracked in minutes; treat the network as open.',
'WPA1 (TKIP) em {n}': 'WPA1 (TKIP) on {n}',
'WPA1/TKIP tem fraquezas conhecidas; migre para WPA2/CCMP ou WPA3.':
'WPA1/TKIP has known weaknesses; migrate to WPA2/CCMP or WPA3.',
'WPS ativo em {n}': 'WPS active on {n}',
'O AP anuncia WPS com configuração travada.': 'The AP advertises WPS with configuration locked.',
'WPS sem lock explícito; PIN pode ser testado com reaver/bully.':
'WPS without an explicit lock; the PIN can be tested with reaver/bully.',
'Sem PMF em {n}': 'No PMF on {n}',
'Desautenticação e desassociação não são protegidas; clientes podem ser expulsos para facilitar a captura de handshake.':
'Deauthentication and disassociation are unprotected; clients can be expelled to make handshake capture easier.',
'PMF inconsistente em {n}': 'Inconsistent PMF on {n}',
'O AP anuncia WPA3, mas não anuncia PMF; verifique a configuração.':
'The AP advertises WPA3 but not PMF; check the configuration.',
'WPA3 com WPS em {n}': 'WPA3 with WPS on {n}',
'WPS não faz parte do WPA3 e abre uma via mais fraca de associação.':
'WPS is not part of WPA3 and opens a weaker association path.',
'primeiro visto': 'first seen',
'associou': 'associated',
'desautenticado': 'deauthenticated',
'A estação voltou a se associar.': 'The station reassociated.',
'O AP pode não usar cache de PMKID.': 'The AP may not cache PMKID.',
'A estação voltou a se associar após a desautenticação: PMF não está obrigatório (ou o cliente não o usa).':
'The station reassociated after deauthentication: PMF is not required (or the client does not use it).',
'A estação continuou trocando quadros depois da desautenticação; o quadro de gerenciamento provavelmente foi aceito.':
'The station kept exchanging frames after deauthentication; the management frame was probably accepted.',
'A estação não voltou a associar na janela observada. PMF pode estar ativo, ou o cliente simplesmente ficou inativo.':
'The station did not reassociate in the observed window. PMF may be active, or the client simply stayed idle.',
'Inconclusivo': 'Inconclusive',
// deauth contínuo e identificação de clientes
'Mantém os quadros saindo em série até você parar (ou até sair o handshake). O canal da captura fica travado no AP durante o job.':
'Keeps frames going until you stop (or a handshake appears). The capture channel stays locked to the AP during the job.',
'Iniciar contínuo': 'Start continuous',
'Parar': 'Stop',
'Rajada ({n})': 'Burst ({n})',
'Pronto. O job contínuo trava a captura no canal do AP e para quando você mandar.':
'Ready. The continuous job locks capture to the AP channel and stops when you say so.',
'Job {n} · {n} quadro(s){n}{n}': 'Job {n} · {n} frame(s){n}{n}',
'Handshake capturado em {n} quadro(s){n}.': 'Handshake captured in {n} frame(s){n}.',
'Job {n} {n} · {n} quadro(s){n}.': 'Job {n} {n} · {n} frame(s){n}.',
'Escolha um cliente para desautenticar nas direções sta/both.':
'Choose a client to deauthenticate in the sta/both directions.',
'Forçando reassociação e handshake…': 'Forcing reassociation and handshake…',
'Desautenticação contínua iniciada.': 'Continuous deauthentication started.',
'Nenhuma ferramenta WPS disponível.': 'No WPS tool available.',
'O reaver embutido usa Pixie Dust sem PIN. O bully aparece se estiver instalado no sistema.':
'The bundled reaver uses Pixie Dust without a PIN. bully appears if it is installed on the system.',
'O reaver embutido não pôde ser extraído nesta execução.':
'The bundled reaver could not be extracted in this run.',
'Dicionário (vazio = embutido, 9,7 milhões de senhas)': 'Wordlist (empty = bundled, 9.7 million passwords)',
'dicionário próprio, opcional': 'custom wordlist, optional',
'Dispositivo': 'Device',
'MAC aleatório': 'Random MAC',
'Cliente {n}': 'Client {n}',
'Linha do tempo · {n}': 'Timeline · {n}',
'nenhuma ferramenta disponível': 'no tool available',
'· canal {n}': '· channel {n}',
'· para sozinho no handshake': '· stops on handshake',
'job de deauth não encontrado': 'deauth job not found',
'já existe um job de desautenticação em execução em {n}':
'a deauthentication job is already running on {n}',
'inicie a captura em {n} antes de iniciar o job':
'start capture on {n} before starting the job',
'a captura em {n} não está ativa': 'capture on {n} is not active',
'{n} não está disponível; o reaver integrado não pôde ser extraído':
'{n} is not available; the bundled reaver could not be extracted',
};
+101 -12
View File
@@ -18,17 +18,39 @@
</div>
</div>
<div class="side-title">Interfaces</div>
<ul class="list" id="ifaceList" data-empty="nenhuma interface Wi-Fi encontrada"></ul>
<div class="side-title">Interface</div>
<div class="iface-pick">
<select id="ifaceSelect" class="select" aria-label="Interface Wi-Fi" data-empty="nenhuma interface Wi-Fi encontrada"></select>
<span id="modePill" class="pill"><span class="dot"></span><span id="modeLabel">—</span></span>
</div>
<div class="side-title">Visualização</div>
<ul class="list" id="viewNav" role="group" aria-label="Visualização">
<li><button type="button" class="dev" data-view="nets" aria-pressed="true">
<span class="ico"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1"/></svg></span>
<span><span class="t">Redes</span><span class="s">APs, clientes e ataques</span></span>
</button></li>
<li><button type="button" class="dev" data-view="channels" aria-pressed="false">
<span class="ico"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="19" x2="5" y2="10"/><line x1="12" y1="19" x2="12" y2="5"/><line x1="19" y1="19" x2="19" y2="13"/></svg></span>
<span><span class="t">Canais</span><span class="s">espectro e saúde</span></span>
</button></li>
</ul>
<div class="side-foot">
<span id="verInfo" class="mono">—</span>
<select id="langSel" class="lang-sel" aria-label="Idioma" title="Idioma">
<option value="auto">Auto</option>
<option value="pt">Português</option>
<option value="en">English</option>
</select>
<button id="btnUpdate" type="button">checar atualização</button>
<div class="foot-row">
<button id="settingsBtn" class="icon-btn" type="button" title="Configurações" aria-label="Configurações">
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>
</button>
<div class="theme-switch" role="group" aria-label="Tema">
<button type="button" data-theme-pick="auto" aria-pressed="true">Auto</button>
<button type="button" data-theme-pick="light" aria-pressed="false">Claro</button>
<button type="button" data-theme-pick="dark" aria-pressed="false">Escuro</button>
</div>
</div>
<div class="foot-row">
<span id="verInfo" class="mono">—</span>
<button id="btnUpdate" type="button">checar atualização</button>
</div>
</div>
</aside>
@@ -39,7 +61,9 @@
<h1>Painel de captura</h1>
<div class="sub" id="ifaceSub">selecione uma interface</div>
</div>
<span id="modePill" class="pill"><span class="dot"></span><span id="modeLabel">—</span></span>
<div class="top-actions">
<button id="btnReport" class="btn" type="button" disabled>Relatório</button>
</div>
</div>
<svg class="wave" id="wave" viewBox="0 0 600 34" preserveAspectRatio="none">
@@ -68,14 +92,79 @@
</div>
<div class="result-head">
<strong>Redes vistas</strong>
<span id="count">0</span>
<div class="result-summary">
<strong id="resultTitle">Redes vistas</strong>
<span id="count">0</span>
</div>
<div class="result-tools">
<input id="searchInput" class="filter" type="search" placeholder="filtrar por SSID ou BSSID" aria-label="Filtrar redes" autocomplete="off" spellcheck="false">
<select id="sortSel" class="select compact" aria-label="Ordenar por">
<option value="signal">Sinal</option>
<option value="security">Segurança</option>
<option value="clients">Clientes</option>
<option value="name">Nome</option>
</select>
</div>
</div>
<div id="resultArea"></div>
</div>
</main>
</div>
<dialog id="targetDlg" class="target-dlg" aria-labelledby="targetTitle">
<div class="target-head">
<div class="target-id">
<h2 id="targetTitle">—</h2>
<div class="target-sub" id="targetSub"></div>
</div>
<button id="targetClose" class="icon-btn" type="button" aria-label="Fechar" title="Fechar">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>
</button>
</div>
<div class="tabs" role="tablist" aria-label="Detalhes do alvo">
<button type="button" role="tab" data-tab="overview" aria-selected="true">Visão geral</button>
<button type="button" role="tab" data-tab="clients" aria-selected="false">Clientes <span class="tab-count" id="tabClientsCount">0</span></button>
<button type="button" role="tab" data-tab="attacks" aria-selected="false">Ataques</button>
</div>
<div class="tab-panel" id="tabPanel"></div>
</dialog>
<dialog id="reportDlg" class="report-dlg" aria-labelledby="reportTitle">
<div class="report-head">
<div>
<h2 id="reportTitle">Relatório de auditoria</h2>
<p class="hint" id="reportStatus">Pré-visualização paginada em A4.</p>
</div>
<div class="report-tools">
<button type="button" class="icon-btn" id="reportZoomOut" title="Diminuir zoom" aria-label="Diminuir zoom">−</button>
<span class="mono" id="reportZoomLabel">100%</span>
<button type="button" class="icon-btn" id="reportZoomIn" title="Aumentar zoom" aria-label="Aumentar zoom">+</button>
<button type="button" class="btn" id="reportPrint">Imprimir / PDF</button>
<button type="button" class="btn" id="reportDownload">Baixar HTML</button>
<button type="button" class="icon-btn" id="reportClose" title="Fechar" aria-label="Fechar">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>
</button>
</div>
</div>
<div class="report-view" id="reportView"><div class="report-canvas" id="reportCanvas"><iframe id="reportFrame" title="Relatório paginado"></iframe></div></div>
</dialog>
<dialog id="settingsDlg" aria-labelledby="settingsTitle">
<form method="dialog">
<h2 id="settingsTitle">Configurações</h2>
<div class="field">
<span class="field-label">Idioma</span>
<select id="langSel" class="select" name="lang" aria-label="Idioma">
<option value="auto">Auto</option>
<option value="pt">Português</option>
<option value="en">English</option>
</select>
</div>
<menu><button class="btn primary" value="close">Fechar</button></menu>
</form>
</dialog>
<script type="module" src="/app.js"></script>
</body>
</html>