");
}
sb.AppendLine("");
diff --git a/Services/ScanHistory.cs b/Services/ScanHistory.cs
index ba0f724a..ba9240e5 100644
--- a/Services/ScanHistory.cs
+++ b/Services/ScanHistory.cs
@@ -24,6 +24,8 @@ internal sealed class HistoricalDevice
public string MacAddress { get; set; } = string.Empty;
public string Vendor { get; set; } = string.Empty;
public string DeviceType { get; set; } = string.Empty;
+ [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)]
+ public DeviceIdentification? Identification { get; set; }
public List OpenPorts { get; set; } = [];
}
@@ -151,6 +153,7 @@ private static bool HasChanged(HistoricalDevice before, HistoricalDevice after)
MacAddress = device.MacAddress,
Vendor = device.Vendor,
DeviceType = device.DeviceType,
+ Identification = device.Identification?.Snapshot(),
OpenPorts = [.. device.OpenPorts.OrderBy(port => port)]
};
diff --git a/Strings/bn.xaml b/Strings/bn.xaml
index 271c1f5b..6021d932 100644
--- a/Strings/bn.xaml
+++ b/Strings/bn.xaml
@@ -506,4 +506,25 @@
এই নামে একটি অবস্থান ইতিমধ্যেই আছে। অন্য নাম বেছে নিন।এই অবস্থানটি পাওয়া যাচ্ছে না বা বদলানো যাচ্ছে না। বিদ্যমান একটি অবস্থান বেছে নিন।অবস্থানের তথ্য পড়া বা সংরক্ষণ করা যায়নি। চালিয়ে যাওয়ার আগে KillerScan-এর অন্য কপিগুলো বন্ধ করে ফাইলের অনুমতি পরীক্ষা করুন অথবা ব্যাকআপ পুনরুদ্ধার করুন।
+ শনাক্তকরণের প্রমাণ
+ নির্ভরযোগ্যতা
+ পাওয়া যায়নি
+ কম
+ মাঝারি
+ বেশি
+ ম্যানুয়াল
+ জানানো মডেল (যাচাই করা হয়নি)
+ পর্যবেক্ষণের সময়সীমা (UTC)
+ উৎস
+ পর্যবেক্ষিত মান
+ পরস্পরবিরোধী সংকেত; প্রতিটি উৎস পরীক্ষা করুন।
+ আগের / ক্যাশে থাকা
+ এটি আনুমানিক মূল্যায়ন, সম্ভাবনা নয়। প্রোটোকলের মান ডিভাইস নিজেই জানায়। MAC নির্মাতা মডেলের প্রমাণ নয়; উত্তর না পাওয়া অনুপস্থিতির প্রমাণ নয়।
+ স্বয়ংক্রিয় ধরন
+ বন্ধ থাকা অনুসন্ধান
+ দ্রুত স্ক্যান
+ পূর্ণ স্ক্যান
+ গভীর স্ক্যান
+ আমদানি করা / পরিসর অনির্দিষ্ট
+ অন্যান্য সম্ভাব্য ধরন
diff --git a/Strings/cs-CZ.xaml b/Strings/cs-CZ.xaml
index defb319a..97cf6387 100644
--- a/Strings/cs-CZ.xaml
+++ b/Strings/cs-CZ.xaml
@@ -504,4 +504,25 @@ Vaše nastavení nebude odstraněno.
Lokalita s tímto názvem již existuje. Zvol jiný název.Tato lokalita není dostupná nebo ji nelze změnit. Vyber existující lokalitu.Data lokality nelze přečíst nebo uložit. Před pokračováním zavři ostatní instance KillerScan a zkontroluj oprávnění k souborům nebo obnov zálohu.
+ Podklady rozpoznání
+ Spolehlivost
+ Není k dispozici
+ Nízká
+ Střední
+ Vysoká
+ Ruční
+ Hlášený model (neověřený)
+ Doba pozorování (UTC)
+ Zdroj
+ Zjištěná hodnota
+ Protichůdné údaje; zkontroluj jednotlivé zdroje.
+ Starší / z mezipaměti
+ Heuristický odhad, nikoli pravděpodobnost. Hodnoty protokolů hlásí samotné zařízení. Výrobce MAC neurčuje model; chybějící odpovědi nedokazují nepřítomnost.
+ Automatický typ
+ Vypnuté dotazy
+ Rychlé skenování
+ Úplné skenování
+ Hloubkové skenování
+ Importováno / rozsah neurčen
+ Další kandidáti
diff --git a/Strings/de-DE.xaml b/Strings/de-DE.xaml
index e6f3011a..545777d4 100644
--- a/Strings/de-DE.xaml
+++ b/Strings/de-DE.xaml
@@ -507,4 +507,25 @@ Ihre Einstellungen bleiben erhalten.
Ein Standort mit diesem Namen existiert bereits. Wähle einen anderen Namen.Dieser Standort ist nicht verfügbar oder kann nicht geändert werden. Wähle einen vorhandenen Standort.Standortdaten konnten nicht gelesen oder gespeichert werden. Schließe andere KillerScan-Instanzen und prüfe die Dateiberechtigungen oder stelle eine Sicherung wieder her, bevor du fortfährst.
+ Erkennungsgrundlagen
+ Zuverlässigkeit
+ Nicht verfügbar
+ Niedrig
+ Mittel
+ Hoch
+ Manuell
+ Gemeldetes Modell (ungeprüft)
+ Erfassungszeitraum (UTC)
+ Quelle
+ Beobachteter Wert
+ Widersprüchliche Hinweise; prüfe die einzelnen Quellen.
+ Früher / zwischengespeichert
+ Heuristische Einschätzung, keine Wahrscheinlichkeit. Protokollwerte werden vom Gerät selbst gemeldet. Der MAC-Hersteller belegt kein Modell; fehlende Antworten belegen keine Abwesenheit.
+ Automatischer Typ
+ Deaktivierte Abfragen
+ Schnellscan
+ Vollständiger Scan
+ Tiefenscan
+ Importiert / Umfang nicht angegeben
+ Weitere Kandidaten
diff --git a/Strings/en-US.xaml b/Strings/en-US.xaml
index 797afd14..19970a5a 100644
--- a/Strings/en-US.xaml
+++ b/Strings/en-US.xaml
@@ -570,4 +570,25 @@ Your settings will not be removed.
A location with this name already exists. Choose another name.This location is unavailable or cannot be changed. Choose an existing location.Location data could not be read or saved. Close other copies of KillerScan and check file permissions or restore a backup before continuing.
+ Recognition evidence
+ Confidence
+ Not available
+ Low
+ Medium
+ High
+ Manual
+ Reported model (unverified)
+ Observation window (UTC)
+ Source
+ Observed value
+ Conflicting signals; check the individual sources.
+ Earlier / cached
+ Heuristic estimate, not a probability. Protocol values are self-reported. MAC vendor does not identify a model; missing responses do not prove absence.
+ Automatic type
+ Disabled probes
+ Quick scan
+ Full scan
+ Deep scan
+ Imported / unspecified scope
+ Other candidates
diff --git a/Strings/es.xaml b/Strings/es.xaml
index deb10cc1..343c0de4 100644
--- a/Strings/es.xaml
+++ b/Strings/es.xaml
@@ -506,4 +506,25 @@ Su configuración no se eliminará.
Ya existe una ubicación con este nombre. Elige otro nombre.Esta ubicación no está disponible o no se puede cambiar. Elige una ubicación existente.No se pudieron leer o guardar los datos de la ubicación. Cierra otras instancias de KillerScan y revisa los permisos de los archivos o restaura una copia de seguridad antes de continuar.
+ Evidencias de identificación
+ Confianza
+ No disponible
+ Baja
+ Media
+ Alta
+ Manual
+ Modelo comunicado (sin verificar)
+ Periodo de observación (UTC)
+ Fuente
+ Valor observado
+ Señales contradictorias; revisa las fuentes individuales.
+ Anterior / en caché
+ Estimación heurística, no una probabilidad. El propio dispositivo comunica los valores de protocolo. El fabricante MAC no identifica un modelo; la falta de respuestas no demuestra ausencia.
+ Tipo automático
+ Consultas desactivadas
+ Análisis rápido
+ Análisis completo
+ Análisis profundo
+ Importado / alcance sin especificar
+ Otros candidatos
diff --git a/Strings/fr-FR.xaml b/Strings/fr-FR.xaml
index f44d248b..4af222f7 100644
--- a/Strings/fr-FR.xaml
+++ b/Strings/fr-FR.xaml
@@ -506,4 +506,25 @@ Vos paramètres ne seront pas supprimés.
Un emplacement porte déjà ce nom. Choisissez un autre nom.Cet emplacement est indisponible ou ne peut pas être modifié. Choisissez un emplacement existant.Impossible de lire ou d'enregistrer les données de l'emplacement. Fermez les autres instances de KillerScan et vérifiez les droits d'accès aux fichiers ou restaurez une sauvegarde avant de continuer.
+ Éléments d'identification
+ Fiabilité
+ Indisponible
+ Faible
+ Moyenne
+ Élevée
+ Manuelle
+ Modèle déclaré (non vérifié)
+ Période d'observation (UTC)
+ Source
+ Valeur observée
+ Indices contradictoires ; vérifiez chaque source.
+ Antérieur / en cache
+ Estimation heuristique, pas une probabilité. Les valeurs des protocoles sont déclarées par l'appareil. Le fabricant MAC ne prouve pas un modèle ; l'absence de réponse ne prouve pas une absence.
+ Type automatique
+ Requêtes désactivées
+ Analyse rapide
+ Analyse complète
+ Analyse approfondie
+ Importé / périmètre non précisé
+ Autres candidats
diff --git a/Strings/hu-HU.xaml b/Strings/hu-HU.xaml
index 99413000..ec3d06b6 100644
--- a/Strings/hu-HU.xaml
+++ b/Strings/hu-HU.xaml
@@ -522,4 +522,25 @@ A beállítások nem törlődnek.
Már létezik ilyen nevű helyszín. Válassz másik nevet.Ez a helyszín nem érhető el vagy nem módosítható. Válassz egy meglévő helyszínt.A helyszín adatai nem olvashatók vagy menthetők. A folytatás előtt zárd be a KillerScan többi példányát, és ellenőrizd a fájljogosultságokat, vagy állíts vissza egy biztonsági másolatot.
+ Felismerési adatok
+ Megbízhatóság
+ Nem érhető el
+ Alacsony
+ Közepes
+ Magas
+ Kézi
+ Jelzett modell (nem ellenőrzött)
+ Megfigyelési időszak (UTC)
+ Forrás
+ Megfigyelt érték
+ Ellentmondó jelek; ellenőrizd az egyes forrásokat.
+ Korábbi / gyorsítótárazott
+ Heurisztikus becslés, nem valószínűség. A protokollértékeket maga az eszköz jelenti. A MAC-gyártó nem bizonyít modellt; a hiányzó válasz nem bizonyít távollétet.
+ Automatikus típus
+ Kikapcsolt lekérdezések
+ Gyors vizsgálat
+ Teljes vizsgálat
+ Mély vizsgálat
+ Importált / nem megadott hatókör
+ További jelöltek
diff --git a/Strings/it-IT.xaml b/Strings/it-IT.xaml
index 8b4f07b4..8e898bf4 100644
--- a/Strings/it-IT.xaml
+++ b/Strings/it-IT.xaml
@@ -522,4 +522,25 @@ Le tue impostazioni non verranno rimosse.
Esiste già una posizione con questo nome. Scegli un altro nome.Questa posizione non è disponibile o non può essere modificata. Scegli una posizione esistente.Impossibile leggere o salvare i dati della posizione. Chiudi le altre istanze di KillerScan e controlla i permessi dei file o ripristina un backup prima di continuare.
+ Elementi di identificazione
+ Affidabilità
+ Non disponibile
+ Bassa
+ Media
+ Alta
+ Manuale
+ Modello dichiarato (non verificato)
+ Periodo di osservazione (UTC)
+ Fonte
+ Valore osservato
+ Segnali contrastanti; controlla le singole fonti.
+ Precedente / in cache
+ Stima euristica, non una probabilità. I valori dei protocolli sono dichiarati dal dispositivo. Il produttore MAC non identifica un modello; la mancanza di risposte non dimostra l'assenza.
+ Tipo automatico
+ Richieste disattivate
+ Scansione rapida
+ Scansione completa
+ Scansione approfondita
+ Importato / ambito non specificato
+ Altri candidati
diff --git a/Strings/ja-JP.xaml b/Strings/ja-JP.xaml
index ee7e13ac..a3599476 100644
--- a/Strings/ja-JP.xaml
+++ b/Strings/ja-JP.xaml
@@ -517,4 +517,25 @@
この名前の拠点はすでに存在します。別の名前を指定してください。この拠点は利用できないか、変更できません。既存の拠点を選択してください。拠点データを読み取りまたは保存できませんでした。続行する前に、他の KillerScan を終了し、ファイルのアクセス権を確認するか、バックアップを復元してください。
+ 識別の根拠
+ 確度
+ 情報なし
+ 低
+ 中
+ 高
+ 手動
+ 申告されたモデル(未検証)
+ 観測期間(UTC)
+ 情報源
+ 観測値
+ 矛盾する情報があります。各情報源を確認してください。
+ 以前の情報 / キャッシュ
+ 推定結果であり、確率ではありません。プロトコルの値は機器による自己申告です。MAC のメーカー名はモデルの証明になりません。応答がないことは不在の証明になりません。
+ 自動判定の種類
+ 無効な照会
+ クイックスキャン
+ フルスキャン
+ 詳細スキャン
+ インポート / 範囲不明
+ その他の候補
diff --git a/Strings/kk-KZ.xaml b/Strings/kk-KZ.xaml
index f7b1de50..a2a2598c 100644
--- a/Strings/kk-KZ.xaml
+++ b/Strings/kk-KZ.xaml
@@ -548,4 +548,25 @@
Осындай атауы бар орналасу орны бұрыннан бар. Басқа атауды таңдаңыз.Бұл орналасу орны қолжетімсіз немесе оны өзгерту мүмкін емес. Бар орналасу орнын таңдаңыз.Орналасу орнының деректерін оқу немесе сақтау мүмкін болмады. Жалғастырмас бұрын KillerScan бағдарламасының басқа даналарын жауып, файл рұқсаттарын тексеріңіз немесе сақтық көшірмені қалпына келтіріңіз.
+ Тану негіздері
+ Сенімділік
+ Қолжетімсіз
+ Төмен
+ Орташа
+ Жоғары
+ Қолмен
+ Хабарланған модель (тексерілмеген)
+ Бақылау кезеңі (UTC)
+ Дереккөз
+ Бақыланған мән
+ Қайшы белгілер; әр дереккөзді тексеріңіз.
+ Бұрынғы / кэштен
+ Ықтималдық емес, эвристикалық баға. Хаттама мәндерін құрылғының өзі хабарлайды. MAC өндірушісі модельді дәлелдемейді; жауаптың болмауы құрылғының жоқтығын дәлелдемейді.
+ Автоматты түр
+ Өшірілген сұраулар
+ Жылдам сканерлеу
+ Толық сканерлеу
+ Терең сканерлеу
+ Импортталған / ауқым көрсетілмеген
+ Басқа нұсқалар
diff --git a/Strings/nb-NO.xaml b/Strings/nb-NO.xaml
index 63c75921..9c0eed8f 100644
--- a/Strings/nb-NO.xaml
+++ b/Strings/nb-NO.xaml
@@ -548,4 +548,25 @@ Innstillingene dine fjernes ikke.
Et sted med dette navnet finnes allerede. Velg et annet navn.Dette stedet er utilgjengelig eller kan ikke endres. Velg et eksisterende sted.Stedsdata kunne ikke leses eller lagres. Lukk andre forekomster av KillerScan og kontroller filrettighetene eller gjenopprett en sikkerhetskopi før du fortsetter.
+ Grunnlag for gjenkjenning
+ Sikkerhet i vurderingen
+ Ikke tilgjengelig
+ Lav
+ Middels
+ Høy
+ Manuell
+ Rapportert modell (ikke bekreftet)
+ Observasjonsperiode (UTC)
+ Kilde
+ Observert verdi
+ Motstridende signaler; kontroller de enkelte kildene.
+ Tidligere / mellomlagret
+ Heuristisk vurdering, ikke en sannsynlighet. Protokollverdier rapporteres av enheten selv. MAC-produsenten beviser ingen modell; manglende svar beviser ikke fravær.
+ Automatisk type
+ Deaktiverte forespørsler
+ Hurtigskanning
+ Full skanning
+ Dybdeskanning
+ Importert / omfang ikke angitt
+ Andre kandidater
diff --git a/Strings/pl-PL.xaml b/Strings/pl-PL.xaml
index 7c09896f..c66f6b9f 100644
--- a/Strings/pl-PL.xaml
+++ b/Strings/pl-PL.xaml
@@ -491,4 +491,25 @@ Twoje ustawienia nie zostaną usunięte.
Lokalizacja o tej nazwie już istnieje. Wybierz inną nazwę.Ta lokalizacja jest niedostępna lub nie można jej zmienić. Wybierz istniejącą lokalizację.Nie można odczytać lub zapisać danych lokalizacji. Przed kontynuowaniem zamknij inne instancje KillerScan i sprawdź uprawnienia do plików lub przywróć kopię zapasową.
+ Podstawy rozpoznania
+ Pewność
+ Niedostępne
+ Niska
+ Średnia
+ Wysoka
+ Ręcznie
+ Zgłoszony model (niezweryfikowany)
+ Okres obserwacji (UTC)
+ Źródło
+ Zaobserwowana wartość
+ Sprzeczne sygnały; sprawdź poszczególne źródła.
+ Wcześniejsze / z pamięci podręcznej
+ Ocena heurystyczna, a nie prawdopodobieństwo. Wartości protokołów są zgłaszane przez urządzenie. Producent MAC nie dowodzi modelu; brak odpowiedzi nie dowodzi nieobecności.
+ Typ automatyczny
+ Wyłączone zapytania
+ Szybkie skanowanie
+ Pełne skanowanie
+ Dokładne skanowanie
+ Importowane / zakres nieokreślony
+ Inne możliwości
diff --git a/Strings/pt-BR.xaml b/Strings/pt-BR.xaml
index e1ea5ae0..1544ee6e 100644
--- a/Strings/pt-BR.xaml
+++ b/Strings/pt-BR.xaml
@@ -548,4 +548,25 @@ Suas configurações não serão removidas.
Já existe um local com esse nome. Escolha outro nome.Este local não está disponível ou não pode ser alterado. Escolha um local existente.Não foi possível ler ou salvar os dados do local. Feche outras instâncias do KillerScan e verifique as permissões dos arquivos ou restaure um backup antes de continuar.
+ Evidências de identificação
+ Confiança
+ Não disponível
+ Baixa
+ Média
+ Alta
+ Manual
+ Modelo informado (não verificado)
+ Período de observação (UTC)
+ Fonte
+ Valor observado
+ Sinais conflitantes; confira cada fonte.
+ Anterior / em cache
+ Estimativa heurística, não uma probabilidade. Os valores dos protocolos são informados pelo próprio dispositivo. O fabricante MAC não identifica um modelo; a falta de respostas não comprova ausência.
+ Tipo automático
+ Consultas desativadas
+ Verificação rápida
+ Verificação completa
+ Verificação profunda
+ Importado / escopo não especificado
+ Outros candidatos
diff --git a/Strings/ru-RU.xaml b/Strings/ru-RU.xaml
index 8a05682b..df8202b2 100644
--- a/Strings/ru-RU.xaml
+++ b/Strings/ru-RU.xaml
@@ -532,4 +532,25 @@
Местоположение с таким именем уже существует. Выберите другое имя.Это местоположение недоступно или не может быть изменено. Выберите существующее местоположение.Не удалось прочитать или сохранить данные местоположения. Перед продолжением закройте другие экземпляры KillerScan и проверьте права доступа к файлам или восстановите резервную копию.
+ Основания распознавания
+ Достоверность
+ Недоступно
+ Низкая
+ Средняя
+ Высокая
+ Вручную
+ Заявленная модель (не проверена)
+ Период наблюдения (UTC)
+ Источник
+ Наблюдаемое значение
+ Противоречивые признаки; проверьте отдельные источники.
+ Прежнее / из кэша
+ Эвристическая оценка, а не вероятность. Значения протоколов сообщает само устройство. Производитель MAC не доказывает модель; отсутствие ответов не доказывает отсутствие устройства.
+ Автоматический тип
+ Отключённые запросы
+ Быстрое сканирование
+ Полное сканирование
+ Глубокое сканирование
+ Импортировано / область не указана
+ Другие варианты
diff --git a/Strings/tr-TR.xaml b/Strings/tr-TR.xaml
index 171f5510..416216f4 100644
--- a/Strings/tr-TR.xaml
+++ b/Strings/tr-TR.xaml
@@ -506,4 +506,25 @@ Ayarlarınız silinmeyecek.
Bu ada sahip bir konum zaten var. Başka bir ad seçin.Bu konum kullanılamıyor veya değiştirilemiyor. Mevcut bir konum seçin.Konum verileri okunamadı veya kaydedilemedi. Devam etmeden önce diğer KillerScan örneklerini kapatın ve dosya izinlerini kontrol edin veya bir yedeği geri yükleyin.
+ Tanıma kanıtları
+ Güven düzeyi
+ Kullanılamıyor
+ Düşük
+ Orta
+ Yüksek
+ Elle
+ Bildirilen model (doğrulanmamış)
+ Gözlem aralığı (UTC)
+ Kaynak
+ Gözlenen değer
+ Çelişkili sinyaller; kaynakları tek tek kontrol edin.
+ Önceki / önbellekte
+ Sezgisel tahmindir, olasılık değildir. Protokol değerlerini cihaz kendisi bildirir. MAC üreticisi modeli kanıtlamaz; yanıt alınmaması yokluğu kanıtlamaz.
+ Otomatik tür
+ Devre dışı sorgular
+ Hızlı tarama
+ Tam tarama
+ Derin tarama
+ İçe aktarılmış / kapsam belirtilmemiş
+ Diğer adaylar
diff --git a/Strings/uk-UA.xaml b/Strings/uk-UA.xaml
index 92deccc8..4efd19b3 100644
--- a/Strings/uk-UA.xaml
+++ b/Strings/uk-UA.xaml
@@ -548,4 +548,25 @@
Розташування з такою назвою вже існує. Виберіть іншу назву.Це розташування недоступне або його неможливо змінити. Виберіть наявне розташування.Не вдалося прочитати або зберегти дані розташування. Перед продовженням закрийте інші екземпляри KillerScan і перевірте дозволи на файли або відновіть резервну копію.
+ Підстави розпізнавання
+ Достовірність
+ Недоступно
+ Низька
+ Середня
+ Висока
+ Вручну
+ Заявлена модель (не перевірена)
+ Період спостереження (UTC)
+ Джерело
+ Спостережене значення
+ Суперечливі ознаки; перевірте окремі джерела.
+ Попереднє / з кешу
+ Евристична оцінка, а не ймовірність. Значення протоколів повідомляє сам пристрій. Виробник MAC не доводить модель; відсутність відповідей не доводить відсутність пристрою.
+ Автоматичний тип
+ Вимкнені запити
+ Швидке сканування
+ Повне сканування
+ Поглиблене сканування
+ Імпортовано / область не вказана
+ Інші варіанти
diff --git a/Strings/vi-VN.xaml b/Strings/vi-VN.xaml
index db19dc46..1177e604 100644
--- a/Strings/vi-VN.xaml
+++ b/Strings/vi-VN.xaml
@@ -487,4 +487,25 @@ Cài đặt của bạn sẽ không bị xóa.
Đã có địa điểm mang tên này. Hãy chọn tên khác.Địa điểm này không khả dụng hoặc không thể thay đổi. Hãy chọn một địa điểm hiện có.Không thể đọc hoặc lưu dữ liệu địa điểm. Trước khi tiếp tục, hãy đóng các phiên bản KillerScan khác và kiểm tra quyền truy cập tệp hoặc khôi phục bản sao lưu.
+ Bằng chứng nhận dạng
+ Độ tin cậy
+ Không có
+ Thấp
+ Trung bình
+ Cao
+ Thủ công
+ Mẫu máy được báo cáo (chưa xác minh)
+ Khoảng thời gian quan sát (UTC)
+ Nguồn
+ Giá trị quan sát
+ Tín hiệu mâu thuẫn; hãy kiểm tra từng nguồn.
+ Trước đó / được lưu đệm
+ Đánh giá theo dấu hiệu, không phải xác suất. Thiết bị tự báo cáo các giá trị giao thức. Nhà sản xuất MAC không chứng minh mẫu máy; không có phản hồi không chứng minh thiết bị vắng mặt.
+ Loại tự động
+ Truy vấn đã tắt
+ Quét nhanh
+ Quét đầy đủ
+ Quét sâu
+ Đã nhập / chưa rõ phạm vi
+ Các khả năng khác
diff --git a/Strings/zh-CN.xaml b/Strings/zh-CN.xaml
index b377ec9d..5b12714f 100644
--- a/Strings/zh-CN.xaml
+++ b/Strings/zh-CN.xaml
@@ -506,4 +506,25 @@
已存在同名位置。请选择其他名称。此位置不可用或无法更改。请选择现有位置。无法读取或保存位置数据。继续前,请关闭其他 KillerScan 实例,检查文件权限或恢复备份。
+ 识别依据
+ 可信程度
+ 无可用信息
+ 低
+ 中
+ 高
+ 手动
+ 设备报告的型号(未验证)
+ 观察时段(UTC)
+ 来源
+ 观察到的值
+ 信号相互矛盾,请检查各个来源。
+ 较早信息 / 缓存
+ 这是启发式估计,不是概率。协议值由设备自行报告。MAC 厂商不能证明型号;没有响应不能证明设备不存在。
+ 自动识别类型
+ 已禁用的查询
+ 快速扫描
+ 完整扫描
+ 深度扫描
+ 已导入 / 范围不明
+ 其他候选类型
diff --git a/Strings/zh-TW.xaml b/Strings/zh-TW.xaml
index 07006d13..46a71360 100644
--- a/Strings/zh-TW.xaml
+++ b/Strings/zh-TW.xaml
@@ -506,4 +506,25 @@
已有同名位置。請選擇其他名稱。此位置無法使用或無法變更。請選擇現有位置。無法讀取或儲存位置資料。繼續前,請關閉其他 KillerScan 執行個體,檢查檔案權限或還原備份。
+ 識別依據
+ 可信程度
+ 無可用資訊
+ 低
+ 中
+ 高
+ 手動
+ 裝置回報的型號(未驗證)
+ 觀察時段(UTC)
+ 來源
+ 觀察到的值
+ 訊號互相矛盾,請檢查各個來源。
+ 較早資訊 / 快取
+ 這是啟發式估計,不是機率。通訊協定值由裝置自行回報。MAC 廠商無法證明型號;沒有回應無法證明裝置不存在。
+ 自動識別類型
+ 已停用的查詢
+ 快速掃描
+ 完整掃描
+ 深入掃描
+ 已匯入 / 範圍不明
+ 其他候選類型
diff --git a/docs/device-identification.md b/docs/device-identification.md
new file mode 100644
index 00000000..773a6a9d
--- /dev/null
+++ b/docs/device-identification.md
@@ -0,0 +1,125 @@
+# Device recognition, evidence and confidence
+
+Right-click a device or service and choose **Recognition evidence**, or press
+**Ctrl+I** with its row focused. The type cell shows the confidence beside the
+existing type. Saved history has the same read-only action. Opening evidence does
+not probe a host, open a connection or refresh a saved result.
+
+The details show the automatic type, contributing source families, alternatives,
+raw observed values, observation times and limitations. A user-assigned type is
+labelled **Manual** separately from the automatic inference. Clearing it restores
+the stored automatic assessment; a user-renamed hostname does not become new
+network evidence. Device names, types and saved assessments retain the existing
+per-location boundaries.
+
+## Meaning of confidence
+
+Confidence applies to the suggested **type/role**, not to ownership, authenticity,
+the exact hardware model or device availability. It is a heuristic grade, not a
+calibrated probability, and no percentage is displayed.
+
+- **Not available**: no sufficient evidence, or an older record without evidence.
+- **Low**: weak hints such as a name, MAC vendor, ports, or conflicting candidates.
+- **Medium**: a specific protocol hint, or the configured gateway/DNS role.
+- **High**: at least two non-reused protocol families support the type, with a
+ specific fingerprint and no close/strong contradictory candidate.
+- **Manual**: a user assignment. The automatic assessment is still visible.
+
+Candidate scoring retains the existing fingerprint catalog, but names contribute
+weak hints instead of short-circuiting the protocol evidence. Repeated rules for
+one source family contribute at most that family's strongest weight (capped at
+15). Candidates need a score of at least 6. Ties use a deterministic type order
+and lower confidence; scores are not probabilities. Network-category hints are
+combined under Switch/AP, as in the previous generic-network result.
+
+Missing replies, no open TCP ports or a locally administered MAC no longer imply
+IoT, Android or Mobile on their own. An uncertain result can therefore change to
+Unknown. OUI/vendor hints may suggest a broad role at low confidence but cannot
+establish a model. Original MAC-prefix corrections are retained and the corrected
+vendor is recorded when used.
+
+All protocol strings are self-reported and may be wrong or spoofed. Even a High
+type grade does not authenticate the device or its hardware. Related services can
+also share an implementation; source-family counting is only a conservative
+heuristic, not a claim of statistical independence.
+
+## Models and time
+
+A model is shown only for an explicit `model=` / `model:` / `modelname=` declaration
+in captured SNMP, HTTP or SSDP values. It remains **reported (unverified)**. This
+first version does not infer a model from a hostname, port, OUI or a product-looking
+title, and does not use an online product catalog. Distinct model declarations
+produce a conflict and no selected model. Declarations longer than 64 characters
+are not truncated into a different model.
+
+Actual scanner results record the per-host observation window in UTC and the
+collection time for used multicast data. mDNS/SSDP reused by a deep probe keeps its
+earlier time and is labelled cached; it cannot count as fresh corroboration for
+High confidence. A hostname's DNS/mDNS/NetBIOS origin is recorded where known.
+Network-wide discovery may precede the per-host activity window; its source time
+is shown separately. An OUI lookup is labelled cached and has no invented live
+observation time. Externally supplied or old data without capture times displays
+them as unavailable. Assessment time and observation time are distinct.
+
+Full, quick and deep modes and explicitly disabled probe choices are saved in the
+assessment. Quick mode does not turn its missing port/fingerprint data into negative
+evidence. Empty fields mean unavailable information; they do not distinguish a
+timeout, a closed port, an unsupported protocol or a skipped operation. This change
+does not add probes or claim per-attempt failure diagnostics.
+
+## Storage, exports and API
+
+`NetworkDevice.Identification` is optional and omitted for unassessed devices.
+Version 1 contains `AutomaticType`, `Confidence`, `ManualType`, `ReportedModel`,
+conflict flags, assessment/observation times, scope, disabled choices, observed
+evidence, matched rules and ranked candidates. Source IDs and types are stable
+language-independent values; UI labels are translated in all 19 supported languages.
+
+`NetworkScanner.IdentifyDevice(device)` assesses supplied fields without I/O,
+global routing hints or saved manual preferences. Existing classification entry
+points still return a type and also populate the optional assessment. Normal
+scanner entry points attach the scan context before the host applies saved names.
+
+History takes a separate snapshot of the assessment, including independent rule
+and candidate source arrays. Optional fields need no migration; older records
+remain readable and are not retroactively assessed. Existing retention still
+applies; evidence increases the size of saved scans. No real user history was
+migrated during development.
+
+- CLI JSON includes the nested assessment. PowerShell preserves it as
+ `.Identification` without adding it to the default table view. The scan/probe
+ wrappers no longer pass `/quiet` (which suppresses JSON too), and an explicit
+ pipeline waits for the Windows GUI-subsystem EXE before parsing the output.
+- Device CSV adds confidence, reported model and a quoted `Identification JSON`
+ field after the existing columns; scripts relying on an exact column count must
+ account for these additions. Service/history CSV also carries the structured data.
+- HTML has expandable evidence within each type cell. All remote values are
+ escaped as text. Observed strings are bounded, control characters are replaced
+ by spaces, and an HTTP title/server share one source family.
+
+The topology and terminal table layouts are unchanged. Structured JSON, CSV,
+HTML and the details window carry the evidence rather than crowding those views.
+
+## Isolated validation and demo
+
+Engine regression checks run on net48 and net10.0 with fabricated observations
+and isolated loopback fixtures. They cover weak hints, contradictory sources,
+duplicate families, explicit/conflicting models, hostile/long values, manual
+assignments, snapshots/JSON, full/quick/deep scope and cached multicast times.
+
+`SpeedTest.Tests.exe --evidence-ui` exercises all 19 language dictionaries,
+focused device/service selection, manual provenance, stored/legacy history and
+escaped exports. It is also included in `--release-ui`. Run UI checks in an isolated
+data root; retained test runners keep fabricated data separate from installed data.
+
+`tests/Test-EvidenceModule.ps1 -ExePath ` tests the real module/EXE JSON
+round trip using `/demo` and a temporary data root, including usage-error propagation
+for an invalid location before any probe can start. The separate scan-options
+module tests retain the mocked argument-forwarding and metadata checks.
+
+`KillerScan.exe --demo` shows fabricated devices. Printer and hypervisor examples
+include synthetic protocol values; preset demo types that disagree with the
+automatic inference are explicitly marked Manual. Demo capture times remain
+unavailable. The CLI `/scan /demo /json` is a safe way to inspect the new JSON shape
+without scanning a network. Neither a demo nor loopback fixtures replace a physical
+pilot or native-speaker review of machine-assisted translations.
diff --git a/tests/KillerScan.Engine.Tests/InterfaceBindingTests.cs b/tests/KillerScan.Engine.Tests/InterfaceBindingTests.cs
index db232ece..c51d7cba 100644
--- a/tests/KillerScan.Engine.Tests/InterfaceBindingTests.cs
+++ b/tests/KillerScan.Engine.Tests/InterfaceBindingTests.cs
@@ -90,7 +90,7 @@ public static Task BoundIcmpAndClassification()
var net = Loopback(); net.Gateway = "127.0.0.1"; net.Dns = "127.0.0.1";
var scanner = new NetworkScanner(new ScanBinding(net, () => true));
string type = (string)typeof(NetworkScanner).GetMethod("ClassifyForScan", BindingFlags.Instance | BindingFlags.NonPublic)!
- .Invoke(scanner, new object[] { new NetworkDevice { IpAddress = "127.0.0.1" } })!;
+ .Invoke(scanner, new object[] { new NetworkDevice { IpAddress = "127.0.0.1" }, new ScanOptions(), "full" })!;
Require(type == "Router/DNS", "An explicitly bound scanner uses its own gateway/DNS, not another tab's globals.");
}
finally { NetworkScanner.GatewayIp = gateway; NetworkScanner.DnsIp = dns; }
diff --git a/tests/KillerScan.Engine.Tests/Program.cs b/tests/KillerScan.Engine.Tests/Program.cs
index a90d9a99..e065fab2 100644
--- a/tests/KillerScan.Engine.Tests/Program.cs
+++ b/tests/KillerScan.Engine.Tests/Program.cs
@@ -13,6 +13,11 @@ private static async Task Main()
await Run("CIDR expansion excludes the network and broadcast addresses", Subnet);
await Run("ARP output parsing keeps IPv4 entries with full MAC addresses", ArpParsing);
await Run("Classifier honors manual types, the gateway, DNS and hostname rules", Classifier);
+ await Run("Recognition keeps weak names, OUI and silent hosts uncertain", RecognitionTests.WeakSignals);
+ await Run("Recognition requires corroboration and exposes contradictory evidence", RecognitionTests.CorroborationAndConflict);
+ await Run("Reported models require explicit declarations and bounded evidence", RecognitionTests.ModelsAndUntrustedValues);
+ await Run("Manual labels, frozen inference, timestamps and JSON remain distinct", RecognitionTests.ManualAndSnapshots);
+ await Run("Full, quick and deep scans retain provenance and cached timestamps", RecognitionTests.ScanProvenance);
await Run("Bundled vendor database loads from the engine assembly", Vendors);
await Run("Status text is English, numeric and free of double hyphens", StatusText);
await Run("Quick scan reports stages, devices and host hooks through a fake platform", QuickScan);
diff --git a/tests/KillerScan.Engine.Tests/RecognitionTests.cs b/tests/KillerScan.Engine.Tests/RecognitionTests.cs
new file mode 100644
index 00000000..39a5ccbc
--- /dev/null
+++ b/tests/KillerScan.Engine.Tests/RecognitionTests.cs
@@ -0,0 +1,117 @@
+using System.Net;
+using System.Text.Json;
+using KillerScan.Engine;
+
+internal static class RecognitionTests
+{
+ private static void Require(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); }
+ private static DeviceIdentification Assess(NetworkDevice device, Func? manual = null)
+ {
+ device.DeviceType = NetworkScanner.ClassifyDeviceForNetwork(device, "", "", manual);
+ return device.Identification!;
+ }
+ internal static Task WeakSignals()
+ {
+ foreach (var device in new[] { new NetworkDevice(), new NetworkDevice { MacAddress = "02:00:00:00:00:01" } })
+ {
+ var result = Assess(device);
+ Require(result.AutomaticType == "Unknown" && result.Confidence == "None", "Silence/private MAC cannot prove IoT or a mobile device.");
+ }
+ var vendor = Assess(new NetworkDevice { Vendor = "Synology" });
+ Require(vendor.AutomaticType == "NAS" && vendor.Confidence == "Low" && vendor.ReportedModel == null,
+ "OUI is a weak role hint, never a model declaration.");
+ var name = Assess(new NetworkDevice { Hostname = "office-synology" });
+ Require(name.AutomaticType == "NAS" && name.Confidence == "Low", "A name alone has low confidence.");
+ var contrary = Assess(new NetworkDevice { Hostname = "synology", HttpTitle = "VMware ESXi", TlsSubject = "CN=VMware ESXi" });
+ Require(contrary.AutomaticType == "Hypervisor" && contrary.Confidence == "High", "Protocol evidence can outweigh a weak name.");
+ var port = Assess(new NetworkDevice { OpenPorts = [8006] });
+ Require(port.AutomaticType == "Hypervisor" && port.Confidence == "Low", "A port alone cannot provide high confidence.");
+ return Task.CompletedTask;
+ }
+ internal static Task CorroborationAndConflict()
+ {
+ var one = Assess(new NetworkDevice { HttpTitle = "Proxmox VMware ESXi XenServer", HttpServer = "Proxmox" });
+ Require(one.AutomaticType == "Hypervisor" && one.Confidence == "Medium", "Repeated hints from HTTP are one source family.");
+ var unrelated = Assess(new NetworkDevice { HttpTitle = "VMware ESXi", TlsSubject = "CN=unrelated fixture" });
+ Require(unrelated.Confidence == "Medium" && unrelated.Candidates.Single().Sources.SequenceEqual(new[] { "HTTP" }),
+ "An unrelated TLS value does not corroborate a matched HTTP rule.");
+ var conflict = Assess(new NetworkDevice { HttpTitle = "VMware ESXi", TlsSubject = "CN=VMware", SnmpDescr = "Printer; model=Unit42" });
+ Require(conflict.ConflictingSignals && conflict.Confidence == "Low", "A strong contradictory source is visible even when its score is lower.");
+ Require(conflict.Rules.Any(rule => rule.Candidate == "Printer" && rule.Sources.Contains("SNMP")), "The alternative has its actual source recorded.");
+ var tie = Assess(new NetworkDevice { SnmpDescr = "Printer", HttpTitle = "Synology" });
+ Require(tie.ConflictingSignals && tie.Confidence == "Low", "Ties cannot silently produce high confidence.");
+ var independent = Assess(new NetworkDevice { SnmpDescr = "Windows Server", HttpTitle = "Unrelated site" });
+ Require(independent.Candidates.Single().Sources.SequenceEqual(new[] { "SNMP" }), "Protocol words in a payload do not create a second source.");
+ Require(Assess(new NetworkDevice { MdnsServices = { "_PRINTER._TCP.LOCAL" } }).AutomaticType == "Printer", "Service recognition is case-insensitive.");
+ return Task.CompletedTask;
+ }
+ internal static Task ModelsAndUntrustedValues()
+ {
+ var named = Assess(new NetworkDevice { Vendor = "Synology", Hostname = "DS999", HttpTitle = "DiskStation DS999" });
+ Require(named.ReportedModel == null, "Model-looking names/titles are not explicit model declarations.");
+ var explicitModel = Assess(new NetworkDevice { SnmpDescr = "Printer; model=Unit42", HttpTitle = "Embedded Web Server; model=Unit42" });
+ Require(explicitModel.ReportedModel == "Unit42" && !explicitModel.ModelConflict, "Matching explicit declarations remain labelled as reported, not verified.");
+ var different = Assess(new NetworkDevice { SnmpDescr = "model=Unit42", SsdpServer = "model=Other99" });
+ Require(different.ReportedModel == null && different.ModelConflict, "Conflicting model declarations do not silently pick one.");
+ var hostile = Assess(new NetworkDevice { HttpTitle = new string('a', 600) + "VMware ESXi", SnmpDescr = "model==HYPERLINK(bad)", SsdpServer = "x\r\n\0", OpenPorts = { 80 } };
+ hostile.DeviceType = NetworkScanner.ClassifyDevice(hostile);
+ string html = ReportExport.BuildHtml(new[] { hostile }, "192.0.2.41", "black");
+ Require(html.Contains("<script>bad()</script>") && !html.Contains("VMware ESXi ");
+ Require(device.Access!.Single().Authentication == "Unknown", "200, data attributes and inactive HTML cannot prove login state.");
+ HttpAccessObservation.Record(device, 9187, false, 401, "WWW-Authenticate: Basic realm=fixture-secret\r\n", "");
+ Require(device.Access!.Single().Authentication == "Required" && device.Access!.Single().Challenge == "Basic" && !JsonSerializer.Serialize(device.Access).Contains("fixture-secret"), "The challenge scheme is evidence; realm/secrets are not retained.");
+ HttpAccessObservation.Record(device, 9187, false, 403, "", "");
+ Require(device.Access!.Single().Authentication == "Unknown", "Forbidden alone does not establish an authentication method.");
+ HttpAccessObservation.Record(device, 9187, false, 200, "WWW-Authenticate: Basic", "");
+ Require(device.Access!.Single().Authentication == "Unknown", "An optional challenge on HTTP 200 does not establish a login requirement.");
+ HttpAccessObservation.Record(device, 9187, false, 302, "Location: http://other.invalid/?token=not-for-storage", "");
+ Require(device.Access!.Single().Redirect && !JsonSerializer.Serialize(device.Access).Contains("not-for-storage"), "Redirects remain evidence without storing target tokens.");
+ var endpoint = device.Access!.Single();
+ Require(DeviceAccess.Address("192.0.2.40;bad", endpoint) == null && DeviceAccess.Address(device.IpAddress, endpoint with { Protocol = "file" }) == null
+ && DeviceAccess.Address(device.IpAddress, endpoint with { Port = -1 }) == null, "Open addresses accept only numeric hosts, valid ports and allowed schemes.");
+ Require(DeviceAccess.Address("2001:db8::40", endpoint) == "http://[2001:db8::40]:9187/", "IPv6 URLs retain brackets and port.");
+ Require(DeviceAccess.ForDevice(new NetworkDevice { IpAddress = "invalid", OpenPorts = [80] }).Count == 0,
+ "Invalid hosts cannot produce inferred actionable endpoints.");
+ var restored = JsonSerializer.Deserialize(JsonSerializer.Serialize(device))!;
+ Require(restored.Access!.Single() == endpoint && DeviceAccess.Web(restored)?.Port == 9187, "JSON and browser selection retain verified nonstandard endpoints.");
+ Require(!JsonSerializer.Serialize(new NetworkDevice()).Contains("Access"), "Old/unassessed devices retain the optional contract.");
+ return Task.CompletedTask;
+ }
+
+ internal static async Task HttpRoundTrip()
+ {
+ using var trap = new Fixture(false, "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
+ using var challenge = new Fixture(false, "HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Digest realm=fixture\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
+ using var redirect = new Fixture(false, "HTTP/1.1 302 Found\r\nLocation: http://127.0.0.1:" + trap.Port + "/\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
+ var device = new NetworkDevice { IpAddress = "127.0.0.1", OpenPorts = [challenge.Port, redirect.Port] };
+ await Probe(device);
+ Require(device.Access?.Count == 2 && device.Access!.Any(item => item.Port == challenge.Port && item.HttpStatus == 401 && item.Authentication == "Required"), "The real probe retains title-less 401 and multiple endpoint results.");
+ Require(device.Access!.Single(item => item.Port == redirect.Port).Redirect && trap.Requests == 0, "Unbound HTTP never follows a redirect or authenticates automatically.");
+ Require(challenge.Requests == 1 && redirect.Requests == 1, "A captured HTTP response needs no second protocol attempt or login retry.");
+ }
+
+ internal static async Task HttpsRoundTrip()
+ {
+ const string body = "Secure fixture";
+ using var fixture = new Fixture(true, "HTTP/1.1 200 OK\r\nContent-Length: " + Encoding.UTF8.GetByteCount(body) + "\r\nConnection: close\r\n\r\n" + body);
+ var device = new NetworkDevice { IpAddress = "127.0.0.1", OpenPorts = [fixture.Port] };
+ await Probe(device);
+ Require(device.Access?.Single() is { Protocol: "https", Authentication: "LoginPage", Verification: "HttpResponse" }, "Deep HTTP detects HTTPS at a nonstandard port with an untrusted fixture certificate. Fixture: " + fixture.LastError + "; access=" + JsonSerializer.Serialize(device.Access));
+ Require(DeviceAccess.Address(device.IpAddress, device.Access!.Single()) == "https://127.0.0.1:" + fixture.Port + "/", "The captured scheme and port drive the address.");
+ }
+
+ internal static async Task StopRequests()
+ {
+ using var stalled = new Fixture(false, "", stall: true);
+ using var next = new Fixture(false, "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
+ using var cancel = new CancellationTokenSource();
+ var task = NetworkScanner.ProbeHttpWithCancellationAsync(new NetworkDevice { IpAddress = "127.0.0.1", OpenPorts = [stalled.Port, next.Port] }, IPAddress.Loopback, true, cancel.Token);
+ var wait = System.Diagnostics.Stopwatch.StartNew();
+ while (stalled.Requests == 0 && wait.ElapsedMilliseconds < 3000) await Task.Delay(10);
+ Require(stalled.Requests == 1, "The controlled stalled HTTP request started."); cancel.Cancel();
+ Require(await Task.WhenAny(task, Task.Delay(1500)) == task, "Stop interrupts the active request promptly.");
+ try { await task; throw new InvalidOperationException("Stop was ignored."); } catch (OperationCanceledException) { }
+ Require(next.Requests == 0, "Stop prevents all later endpoint attempts.");
+ }
+
+ private static Task Probe(NetworkDevice device) => (Task)typeof(NetworkScanner)
+ .GetMethod("ProbeHttpAsync", BindingFlags.NonPublic | BindingFlags.Static)!.Invoke(null, new object[] { device, IPAddress.Loopback, true })!;
+
+ private sealed class Fixture : IDisposable
+ {
+ private readonly TcpListener _listener = new(IPAddress.Loopback, 0);
+ private readonly CancellationTokenSource _stop = new();
+ private readonly System.Collections.Concurrent.ConcurrentBag _clients = [];
+ private readonly X509Certificate2? _certificate;
+ private readonly string _response;
+ private readonly bool _stall;
+ internal int Port { get; }
+ internal int Requests;
+ internal string? LastError;
+ internal Fixture(bool tls, string response, bool stall = false)
+ {
+ _response = response;
+ _stall = stall;
+ if (tls)
+ {
+ using var rsa = RSA.Create(); rsa.KeySize = 2048;
+ var request = new CertificateRequest("CN=localhost", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+ using var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1));
+#if NET10_0_OR_GREATER
+ _certificate = X509CertificateLoader.LoadPkcs12(certificate.Export(X509ContentType.Pfx), null, X509KeyStorageFlags.DefaultKeySet);
+#else
+ _certificate = new X509Certificate2(certificate.Export(X509ContentType.Pfx), (string?)null, X509KeyStorageFlags.DefaultKeySet);
+#endif
+ }
+ _listener.Start(); Port = ((IPEndPoint)_listener.LocalEndpoint).Port; _ = Accept();
+ }
+ private async Task Accept()
+ {
+ try { while (!_stop.IsCancellationRequested) { var client = await _listener.AcceptTcpClientAsync(); _clients.Add(client); _ = Serve(client); } }
+ catch (Exception error) when (error is SocketException or ObjectDisposedException) { }
+ }
+ private async Task Serve(TcpClient client)
+ {
+ using (client)
+ try
+ {
+ System.IO.Stream stream = client.GetStream();
+ using var ssl = _certificate == null ? null : new SslStream(stream);
+ if (ssl != null) { await ssl.AuthenticateAsServerAsync(_certificate!, false, SslProtocols.Tls12, false); stream = ssl; }
+ var buffer = new byte[4096]; int used = 0;
+ while (used < buffer.Length)
+ {
+ int count = await stream.ReadAsync(buffer, used, buffer.Length - used, _stop.Token);
+ if (count == 0) return; used += count;
+ if (Encoding.ASCII.GetString(buffer, 0, used).Contains("\r\n\r\n")) break;
+ }
+ string request = Encoding.ASCII.GetString(buffer, 0, used);
+ if (!request.StartsWith("GET / HTTP/", StringComparison.Ordinal)) return;
+ Require(!request.Contains("Authorization:") && !request.Contains("Cookie:"), "Fixtures receive no credentials/cookies.");
+ Interlocked.Increment(ref Requests);
+ if (_stall) { await Task.Delay(Timeout.Infinite, _stop.Token); return; }
+ byte[] bytes = Encoding.UTF8.GetBytes(_response); await stream.WriteAsync(bytes, 0, bytes.Length, _stop.Token);
+ }
+ catch (Exception error) when (error is System.IO.IOException or AuthenticationException or OperationCanceledException or ObjectDisposedException) { LastError = error.ToString(); }
+ }
+ public void Dispose() { _stop.Cancel(); _listener.Stop(); foreach (var client in _clients) client.Dispose(); _certificate?.Dispose(); _stop.Dispose(); }
+ }
+}
diff --git a/tests/KillerScan.Engine.Tests/InterfaceBindingTests.cs b/tests/KillerScan.Engine.Tests/InterfaceBindingTests.cs
index c51d7cba..13bd6dc9 100644
--- a/tests/KillerScan.Engine.Tests/InterfaceBindingTests.cs
+++ b/tests/KillerScan.Engine.Tests/InterfaceBindingTests.cs
@@ -106,9 +106,9 @@ public static async Task BoundHttp()
using var trap = new Listener(IPAddress.Parse("127.0.0.2"), 0);
trap.Start();
int trapPort = ((IPEndPoint)trap.LocalEndpoint).Port;
- async Task Serve(string response)
+ async Task Serve(string response, Listener? source = null)
{
- using var client = await listener.AcceptTcpClientAsync();
+ using var client = await (source ?? listener).AcceptTcpClientAsync();
Require(((IPEndPoint)client.Client.RemoteEndPoint!).Address.Equals(IPAddress.Loopback), "HTTP uses the selected source address.");
using var reader = new StreamReader(client.GetStream(), Encoding.ASCII, false, 4096, leaveOpen: true);
while (await reader.ReadLineAsync() is { Length: > 0 }) { }
@@ -120,10 +120,22 @@ async Task Serve(string response)
await Probes(run).Http(device, IPAddress.Loopback, true);
Require(await Task.WhenAny(serve, Task.Delay(2000)) == serve, "HTTP fixture received the request."); await serve;
Require(device.HttpTitle == "Bound title" && device.HttpServer == "Bound-Fixture", "Bound HTTP retains title and server fingerprints.");
+ Require(device.Access?.Single().Port == port && device.Access!.Single().HttpStatus == 200,
+ "Bound HTTP stores the actual endpoint response.");
serve = Serve("HTTP/1.1 302 Found\r\nServer: Redirect-Fixture\r\nLocation: http://127.0.0.2:" + trapPort + "/\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
await Probes(run).Http(new NetworkDevice { OpenPorts = [port] }, IPAddress.Loopback, true);
Require(await Task.WhenAny(serve, Task.Delay(2000)) == serve, "Redirect fixture received the request."); await serve;
Require(!trap.Pending(), "Bound fingerprinting never follows redirects to another endpoint.");
+ using var second = new Listener(IPAddress.Loopback, 0); second.Start();
+ int secondPort = ((IPEndPoint)second.LocalEndpoint).Port;
+ var firstReply = Serve("HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Basic realm=fixture\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
+ var secondReply = Serve("HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", second);
+ var multiple = new NetworkDevice { IpAddress = "127.0.0.1", OpenPorts = [port, secondPort] };
+ await Probes(run).Http(multiple, IPAddress.Loopback, true);
+ await Task.WhenAll(firstReply, secondReply);
+ Require(multiple.Access?.Count == 2 && multiple.Access!.Any(item => item.Port == port && item.Authentication == "Required")
+ && multiple.Access!.Any(item => item.Port == secondPort && item.Authentication == "LoginPage"),
+ "Bound probes preserve multiple endpoint-specific authentication observations: " + System.Text.Json.JsonSerializer.Serialize(multiple.Access));
}
public static async Task BoundDnsRoundTrip()
diff --git a/tests/KillerScan.Engine.Tests/Program.cs b/tests/KillerScan.Engine.Tests/Program.cs
index 4041534c..62b3de34 100644
--- a/tests/KillerScan.Engine.Tests/Program.cs
+++ b/tests/KillerScan.Engine.Tests/Program.cs
@@ -15,6 +15,10 @@ private static async Task Main()
await Run("ARP output parsing keeps IPv4 entries with full MAC addresses", ArpParsing);
await Run("Classifier honors manual types, the gateway, DNS and hostname rules", Classifier);
await Run("Recognition keeps weak names, OUI and silent hosts uncertain", RecognitionTests.WeakSignals);
+ await Run("Access keeps endpoint evidence, login uncertainty and safe addresses", AccessTests.Evidence);
+ await Run("Real HTTP retains multiple endpoints and refuses redirects/auth retries", AccessTests.HttpRoundTrip);
+ await Run("Deep access records real HTTPS on a nonstandard port", AccessTests.HttpsRoundTrip);
+ await Run("Stop cancels HTTP and prevents later access probes", AccessTests.StopRequests);
await Run("Recognition requires corroboration and exposes contradictory evidence", RecognitionTests.CorroborationAndConflict);
await Run("Reported models require explicit declarations and bounded evidence", RecognitionTests.ModelsAndUntrustedValues);
await Run("Manual labels, frozen inference, timestamps and JSON remain distinct", RecognitionTests.ManualAndSnapshots);
@@ -100,7 +104,7 @@ private static Task Subnet()
try { NetworkScanner.GetAddressesInSubnet(cidr); }
catch (ArgumentOutOfRangeException ex)
{
- rejected = ex.ParamName == "cidr" && ex.Message.Contains("65,536", StringComparison.Ordinal);
+ rejected = ex.ParamName == "cidr" && ex.Message.Contains(ScanTargets.MaxAddresses.ToString("N0"), StringComparison.Ordinal);
}
Require(rejected, cidr + " should be rejected before its hosts are allocated");
}
diff --git a/tests/KillerScan.Engine.Tests/RecognitionTests.cs b/tests/KillerScan.Engine.Tests/RecognitionTests.cs
index 39a5ccbc..18b7f98d 100644
--- a/tests/KillerScan.Engine.Tests/RecognitionTests.cs
+++ b/tests/KillerScan.Engine.Tests/RecognitionTests.cs
@@ -92,7 +92,7 @@ internal override void FlushDns() { }
internal override Task> Mdns(CancellationToken token) => Task.FromResult(new Dictionary
{ ["192.0.2.10"] = new() { Services = { "_printer._tcp.local" } } });
internal override Task> Ssdp(CancellationToken token) => Task.FromResult(new Dictionary { ["192.0.2.10"] = "model=Unit42" });
- internal override Task Http(NetworkDevice device, IPAddress address, bool deep) { device.HttpTitle = "Embedded Web Server; model=Unit42"; return Task.CompletedTask; }
+ internal override Task Http(NetworkDevice device, IPAddress address, bool deep, CancellationToken token = default) { device.HttpTitle = "Embedded Web Server; model=Unit42"; return Task.CompletedTask; }
internal override Task Tls(NetworkDevice device, IPAddress address, bool deep) => Task.CompletedTask;
internal override Task Ssh(NetworkDevice device, IPAddress address) => Task.CompletedTask;
internal override Task Snmp(NetworkDevice device, IPAddress address) { device.SnmpDescr = "Printer; model=Unit42"; return Task.CompletedTask; }
diff --git a/tests/KillerScan.Engine.Tests/ScanOptionsTests.cs b/tests/KillerScan.Engine.Tests/ScanOptionsTests.cs
index 02e95063..a0685270 100644
--- a/tests/KillerScan.Engine.Tests/ScanOptionsTests.cs
+++ b/tests/KillerScan.Engine.Tests/ScanOptionsTests.cs
@@ -10,6 +10,7 @@ private sealed class Probes : NetworkScanner.ScanProbes
{
internal readonly ConcurrentDictionary Calls = new();
internal bool FailMulticast;
+ internal HashSet ResponsivePorts = [80, 443, 22];
private void Hit(string name) => Calls.AddOrUpdate(name, 1, (_, count) => count + 1);
internal override IReadOnlyDictionary ReadNeighbors()
{ Hit("neighbor-cache"); return new Dictionary { [Host] = "" }; }
@@ -20,7 +21,7 @@ internal override IReadOnlyDictionary ReadNeighbors()
internal override Task Ping(IPAddress address, int timeout) { Hit("icmp"); return Task.FromResult(null); }
internal override Task TcpAlive(IPAddress address) { Hit("tcp-liveness"); return Task.FromResult(true); }
internal override Task Connect(IPAddress address, int port, int timeout)
- { Hit("ports"); return Task.FromResult(port is 80 or 443 or 22); }
+ { Hit("ports"); return Task.FromResult(ResponsivePorts.Contains(port)); }
internal override Task> Mdns(CancellationToken ct)
{
Hit("mdns");
@@ -30,7 +31,7 @@ internal override Task Connect(IPAddress address, int port, int timeout)
}
internal override Task> Ssdp(CancellationToken ct)
{ Hit("ssdp"); return Task.FromResult(new Dictionary { [Host] = "Fixture" }); }
- internal override Task Http(NetworkDevice device, IPAddress address, bool deep) { Hit("http"); return Task.CompletedTask; }
+ internal override Task Http(NetworkDevice device, IPAddress address, bool deep, CancellationToken token = default) { Hit("http"); return Task.CompletedTask; }
internal override Task Tls(NetworkDevice device, IPAddress address, bool deep) { Hit("tls"); return Task.CompletedTask; }
internal override Task Ssh(NetworkDevice device, IPAddress address) { Hit("ssh"); return Task.CompletedTask; }
internal override Task Snmp(NetworkDevice device, IPAddress address) { Hit("snmp"); return Task.CompletedTask; }
@@ -77,6 +78,16 @@ public static async Task CompatibilityAndIsolation()
Require(defaults.Calls.ContainsKey(feature), "The existing API still enables " + feature);
Require(defaults.Calls.ContainsKey("dns-flush"), "The existing API keeps the DNS cache flush.");
+ foreach (int port in new[] { 5357, 32400 })
+ {
+ var onlyWeb = new Probes { ResponsivePorts = [port] };
+ await new NetworkScanner(onlyWeb).ScanSubnetAsync(Host, CancellationToken.None);
+ Require(onlyWeb.Calls.ContainsKey("http"), "A service alone on port " + port + " starts the normal HTTP fingerprint.");
+ var disabled = new Probes { ResponsivePorts = [port] };
+ await new NetworkScanner(disabled).ScanSubnetAsync(Host, CancellationToken.None, new ScanOptions().Without("http"));
+ Require(!disabled.Calls.ContainsKey("http"), "The additional web port still respects HTTP off.");
+ }
+
var probes = new Probes();
var scanner = new NetworkScanner(probes);
var first = await scanner.ScanSubnetAsync(Host, CancellationToken.None);
diff --git a/tests/SpeedTest.Tests/AccessUiChecks.cs b/tests/SpeedTest.Tests/AccessUiChecks.cs
new file mode 100644
index 00000000..a4d18971
--- /dev/null
+++ b/tests/SpeedTest.Tests/AccessUiChecks.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using System.Windows;
+using System.Windows.Controls;
+using KillerScan.Controls;
+using KillerScan.Engine;
+using KillerScan.Services;
+using KillerScan.Shell;
+using AccessText = KillerScan.Services.AccessText;
+
+internal static class AccessUiChecks
+{
+ private const BindingFlags Instance = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
+ private static void Require(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); }
+ private static System.Collections.Generic.IEnumerable Children(DependencyObject parent) where T : DependencyObject
+ {
+ foreach (var child in LogicalTreeHelper.GetChildren(parent).OfType())
+ {
+ if (child is T found) yield return found;
+ foreach (var nested in Children(child)) yield return nested;
+ }
+ }
+ internal static void Run(MainWindow window, Action capture)
+ {
+ Require(DemoData.Enabled, "Access fixtures require isolated demo history.");
+ var endpoint = new AccessEndpoint { Protocol = "https", Port = 9187, Verification = "HttpResponse", Authentication = "Required", Challenge = "Basic", HttpStatus = 401,
+ ObservedAtUtc = new DateTimeOffset(2026, 10, 20, 8, 0, 0, TimeSpan.Zero) };
+ var device = new NetworkDevice { IpAddress = "192.0.2.40", Hostname = "access-fixture", OpenPorts = { 9187, 53 }, Access = new() { endpoint } };
+ var old = ScanHistory.Entries.ToArray();
+ var stored = ScanHistory.Record(device.IpAddress, new[] { device }).Current.Devices.Single();
+ device.Access!.Clear();
+ Require(stored.Access!.Single() == endpoint, "History owns its access list snapshot.");
+ device.Access.Add(endpoint); ScanHistory.SeedDemo(old);
+ string csv = ReportExport.BuildCsv(new[] { device });
+ string html = ReportExport.BuildHtml(new[] { device }, device.IpAddress, "black");
+ Require(csv.Contains("Access JSON") && csv.Contains("9187") && html.Contains("https://192.0.2.40:9187/"), "Exports retain the captured endpoint.");
+ var workspace = (ScanWorkspace)typeof(MainWindow).GetField("_scanWorkspace", Instance)!.GetValue(window)!;
+ var actionField = workspace.GetType().GetField("DeviceAction", Instance)!;
+ object? original = actionField.GetValue(workspace); actionField.SetValue(workspace, null);
+ ScanDeviceActionEventArgs? raised = null;
+ workspace.DeviceAction += (_, e) => raised = e;
+ try
+ {
+ workspace.OpenAccess(device, endpoint);
+ Require(raised?.Action == "Browser" && raised.ServicePort == 9187 && ReferenceEquals(raised.Access, endpoint), "The existing open action receives the selected exact endpoint.");
+ raised = null; workspace.OpenAccess(device, endpoint with { Protocol = "file" });
+ Require(raised == null, "Unsupported schemes cannot launch a process.");
+ workspace.Session.Devices.Add(device); workspace.SetView("services");
+ var services = (DataGrid)workspace.FindName("ServicesGrid");
+ var row = services.Items.Cast