diff --git a/analysis/classes/report.py b/analysis/classes/report.py index 8a601dc..9146bb2 100644 --- a/analysis/classes/report.py +++ b/analysis/classes/report.py @@ -2,10 +2,13 @@ import weasyprint import os import json import hashlib +import re +import sys from weasyprint import HTML from pathlib import Path from datetime import datetime +from utils import get_config class Report(object): @@ -28,6 +31,14 @@ class Report(object): except: self.capture_sha1 = "N/A" + self.userlang = get_config(("frontend", "user_lang")) + + # Load template language + if not re.match("^[a-z]{2,3}$", self.userlang): + self.userlang = "en" + with open(os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), "locales/{}.json".format(self.userlang))) as f: + self.template = json.load(f)["report"] + def read_json(self, json_path): """ Read and convert a JSON file. @@ -59,21 +70,35 @@ class Report(object): :return: str """ if len(self.alerts["high"]): - return "
Your device seems to be compromised as you have {} high alert(s).
".format(self.nb_translate(len(self.alerts["high"]))) + msg = "
" + msg += self.template["high_msg"].format( + self.nb_translate(len(self.alerts["high"]))) + msg += "
" + return msg elif len(self.alerts["moderate"]): - return "
You have {} moderate alert(s), your device might be compromised. Please look at them carefully.
".format(self.nb_translate(len(self.alerts["moderate"]))) + msg = "
" + msg += self.template["moderate_msg"].format( + self.nb_translate(len(self.alerts["moderate"]))) + msg += "
" + return msg elif len(self.alerts["low"]): - return "
You have only {} low alert(s), don't hesitate to check them.
".format(self.nb_translate(len(self.alerts["low"]))) + msg = "
" + msg += self.template["low_msg"].format( + self.nb_translate(len(self.alerts["low"]))) + msg += "
" + return msg else: - return "
Everything looks fine, zero alerts. Don't hesitate to check the uncategorized communications, if any.
" + msg = "
" + msg += self.template["none_msg"] + msg += "
" + return msg def nb_translate(self, nb): """ Translate a number in a string. :return: str """ - a = ["one", "two", "three", "four", "five", - "six", "seven", "height", "nine"] + a = self.template["numbers"] return a[nb-1] if nb <= 9 else str(nb) def generate_suspect_conns_block(self): @@ -85,17 +110,17 @@ class Report(object): if not len([c for c in self.conns if c["alert_tiggered"] == True]): return "" - title = "

Suspect communications

" - table = """ - - - - - - - - - """ + title = "

{}

".format(self.template["suspect_title"]) + table = "
ProtocolDomainDst IP AddressDst port
" + table += " " + table += " " + table += " ".format(self.template["protocol"]) + table += " ".format(self.template["domain"]) + table += " ".format(self.template["dst_ip"]) + table += " ".format(self.template["dst_port"]) + table += " " + table += " " + table += "" for rec in self.conns: if rec["alert_tiggered"] == True: @@ -117,17 +142,17 @@ class Report(object): if not len([c for c in self.conns if c["alert_tiggered"] == False]): return "" - title = "

Uncategorized communications

" - table = """
{}{}{}{}
- - - - - - - - - """ + title = "

{}

".format(self.template["uncat_title"]) + table = "
ProtocolDomainDst IP AddressDst port
" + table += " " + table += " " + table += " ".format(self.template["protocol"]) + table += " ".format(self.template["domain"]) + table += " ".format(self.template["dst_ip"]) + table += " ".format(self.template["dst_port"]) + table += " " + table += " " + table += "" for rec in self.conns: if rec["alert_tiggered"] == False: @@ -149,17 +174,17 @@ class Report(object): if not len(self.whitelist): return "" - title = "

Whitelisted communications

" - table = """
{}{}{}{}
- - - - - - - - - """ + title = "

{}

".format(self.template["whitelist_title"]) + table = "
ProtocolDomainDst IP AddressDst port
" + table += " " + table += " " + table += " ".format(self.template["protocol"]) + table += " ".format(self.template["domain"]) + table += " ".format(self.template["dst_ip"]) + table += " ".format(self.template["dst_port"]) + table += " " + table += " " + table += "" for rec in sorted(self.whitelist, key=lambda k: k['resolution']): table += "" @@ -179,17 +204,18 @@ class Report(object): """ header = "
" header += "
" - header += "


Device name: {}
".format( - self.device["name"]) - header += "Device MAC address: {}
".format( - self.device["mac_address"]) - header += "Report generated on {}
".format( - datetime.now().strftime("%d/%m/%Y at %H:%M:%S")) - header += "Capture duration: {}s
".format( - self.capinfos["Capture duration"]) - header += "Number of packets: {}
".format( - self.capinfos["Number of packets"]) - header += "Capture SHA1: {}
".format(self.capture_sha1) + header += "


{}: {}
".format(self.template["device_name"], + self.device["name"]) + header += "{}: {}
".format(self.template["device_mac"], + self.device["mac_address"]) + header += "{} {}
".format(self.template["report_generated_on"], + datetime.now().strftime("%d/%m/%Y - %H:%M:%S")) + header += "{}: {}s
".format(self.template["capture_duration"], + self.capinfos["Capture duration"]) + header += "{}: {}
".format(self.template["packets_number"], + self.capinfos["Number of packets"]) + header += "{}: {}
".format( + self.template["capture_sha1"], self.capture_sha1) header += "

" header += "
" return header @@ -420,16 +446,16 @@ class Report(object): } @page { @top-center { - content: "REPORT_HEADER - Page " counter(page) " of " counter(pages) "."; + content: "REPORT_HEADER - Page " counter(page) " / " counter(pages) "."; font-size:12px; color:#CCC; } @bottom-center { - content: "This report has been autogenerated by a Tinycheck device. For any question, bug report or feedback, please contact tinycheck@kaspersky.com."; + content: "REPORT_FOOTER"; font-size:12px; color:#CCC; } } - """.replace("REPORT_HEADER", "Report for the capture {}".format(self.capture_sha1)) + """.replace("REPORT_HEADER", "{} {}".format(self.template["report_for_the_capture"], self.capture_sha1)).replace("REPORT_FOOTER", self.template["report_footer"]) diff --git a/analysis/classes/suricataengine.py b/analysis/classes/suricataengine.py index 98db0a2..fb7edd9 100644 --- a/analysis/classes/suricataengine.py +++ b/analysis/classes/suricataengine.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from utils import get_iocs, get_apname, get_device +from utils import get_iocs, get_apname, get_device, get_config import time import os import subprocess as sp import re import json +import sys class SuricataEngine(): @@ -20,6 +21,14 @@ class SuricataEngine(): self.rules = [r[0] for r in get_iocs( "snort")] + self.generate_contextual_alerts() + self.userlang = get_config(("frontend", "user_lang")) + + # Load template language + if not re.match("^[a-z]{2,3}$", self.userlang): + self.userlang = "en" + with open(os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), "locales/{}.json".format(self.userlang))) as f: + self.template = json.load(f)["alerts"] + def start_suricata(self): """ Launch suricata against the capture.pcap file. @@ -37,9 +46,8 @@ class SuricataEngine(): s = line.split("[**]")[1].strip() m = re.search( r"\[\d+\:(?P\d+)\:(?P\d+)\] (?P[ -~]+)", s) - self.alerts.append({"title": "Suricata rule tiggered: {}".format(m.group('title')), - "description": """A network detection rule has been tiggered. It's likely that your device has been compromised - or contains a malicious application.""", + self.alerts.append({"title": self.template["SNORT-01"]["title"].format(m.group('title')), + "description": self.template["SNORT-01"]["description"], "level": "High", "id": "SNORT-01"}) # Remove fast.log @@ -48,7 +56,7 @@ class SuricataEngine(): def generate_rule_file(self): """ Generate the rules file passed to suricata. - :return: bool if operation succeed. + :return: bool if operation succeed. """ try: with open(self.rules_file, "w+") as f: diff --git a/analysis/classes/zeekengine.py b/analysis/classes/zeekengine.py index eac38ce..4ce60a5 100644 --- a/analysis/classes/zeekengine.py +++ b/analysis/classes/zeekengine.py @@ -10,6 +10,8 @@ import subprocess as sp import json import pydig import os +import re +import sys class ZeekEngine(object): @@ -24,10 +26,17 @@ class ZeekEngine(object): self.files = [] self.whitelist = [] - # Get analysis configuration + # Get analysis and userlang configuration self.heuristics_analysis = get_config(("analysis", "heuristics")) self.iocs_analysis = get_config(("analysis", "iocs")) self.whitelist_analysis = get_config(("analysis", "whitelist")) + self.userlang = get_config(("frontend", "user_lang")) + + # Load template language + if not re.match("^[a-z]{2,3}$", self.userlang): + self.userlang = "en" + with open(os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), "locales/{}.json".format(self.userlang))) as f: + self.template = json.load(f)["alerts"] def fill_dns(self, dir): """ @@ -99,47 +108,41 @@ class ZeekEngine(object): # Check for UDP / ICMP (strange from a smartphone.) if c["proto"] in ["UDP", "ICMP"]: c["alert_tiggered"] = True - self.alerts.append({"title": "{} communication going outside the local network to {}.".format(c["proto"].upper(), c["resolution"]), - "description": "The {} protocol is commonly used in internal networks. Please, verify if the host {} leveraged other alerts which may ".format(c["proto"].upper(), c["resolution"]) - + "indicates a possible malicious behavior.", - "host": c["resolution"], + self.alerts.append({"title": self.template["PROTO-01"]["title"].format(c["proto"].upper(), c["resolution"]), + "description": self.template["PROTO-01"]["description"].format(c["proto"].upper(), c["resolution"]), + "host": c["resolution"], "level": "Moderate", "id": "PROTO-01"}) # Check for use of ports over 1024. if c["port_dst"] >= max_ports: c["alert_tiggered"] = True - self.alerts.append({"title": "{} connection to {} to a port over or equal to {}.".format(c["proto"].upper(), c["resolution"], max_ports), - "description": "{} connections have been seen to {} by using the port {}. The use of non-standard port can be sometimes associated to malicious activities. ".format(c["proto"].upper(), c["resolution"], c["port_dst"]) - + "We recommend to check if this host has a good reputation by looking on other alerts and search it on the internet.", - "host": c["resolution"], + self.alerts.append({"title": self.template["PROTO-02"]["title"].format(c["proto"].upper(), c["resolution"], max_ports), + "description": self.template["PROTO-02"]["description"].format(c["proto"].upper(), c["resolution"], c["port_dst"]), + "host": c["resolution"], "level": "Low", "id": "PROTO-02"}) # Check for use of HTTP. if c["service"] == "http" and c["port_dst"] == http_default_port: c["alert_tiggered"] = True - self.alerts.append({"title": "HTTP communications have been done to the host {}".format(c["resolution"]), - "description": "Your device exchanged with the host {} by using HTTP, an unencrypted protocol. ".format(c["resolution"]) - + "Even if this behavior is not malicious by itself, it is unusual to see HTTP communications issued from smartphone applications " - + "running in the background. Please check the host reputation by searching it on the internet.", - "host": c["resolution"], + self.alerts.append({"title": self.template["PROTO-03"]["title"].format(c["resolution"]), + "description": self.template["PROTO-03"]["description"].format(c["resolution"]), + "host": c["resolution"], "level": "Low", "id": "PROTO-03"}) # Check for use of HTTP on a non standard port. if c["service"] == "http" and c["port_dst"] != http_default_port: c["alert_tiggered"] = True - self.alerts.append({"title": "HTTP communications have been seen to the host {} on a non standard port ({}).".format(c["resolution"], c["port_dst"]), - "description": "Your device exchanged with the host {} by using HTTP, an unencrypted protocol on the port {}. ".format(c["resolution"], c["port_dst"]) - + "This behavior is quite unusual. Please check the host reputation by searching it on the internet.", - "host": c["resolution"], + self.alerts.append({"title": self.template["PROTO-04"]["title"].format(c["resolution"], c["port_dst"]), + "description": self.template["PROTO-04"]["description"].format(c["resolution"], c["port_dst"]), + "host": c["resolution"], "level": "Moderate", "id": "PROTO-04"}) # Check for non-resolved IP address. if c["ip_dst"] == c["resolution"]: c["alert_tiggered"] = True - self.alerts.append({"title": "The server {} hasn't been resolved by any DNS query during the session".format(c["ip_dst"]), - "description": "It means that the server {} is likely not resolved by any domain name or the resolution has already been cached by ".format(c["ip_dst"]) - + "the device. If the host appears in other alerts, please check it.", + self.alerts.append({"title": self.template["PROTO-05"]["title"].format(c["ip_dst"]), + "description": self.template["PROTO-05"]["description"].format(c["ip_dst"]), "host": c["ip_dst"], "level": "Low", "id": "PROTO-05"}) @@ -159,10 +162,8 @@ class ZeekEngine(object): for host in bl_hosts: if c["ip_dst"] == host[0]: c["alert_tiggered"] = True - self.alerts.append({"title": "A connection has been made to {} ({}) which is tagged as {}.".format(c["resolution"], c["ip_dst"], host[1].upper()), - "description": "The host {} has been explicitly blacklisted for malicious activities. Your device is likely compromised ".format(c["ip_dst"]) - + "and needs to be investigated more deeply by IT security professionals.", - + self.alerts.append({"title": self.template["IOC-01"]["title"].format(c["resolution"], c["ip_dst"], host[1].upper()), + "description": self.template["IOC-01"]["description"].format(c["ip_dst"]), "host": c["resolution"], "level": "High", "id": "IOC-01"}) @@ -171,10 +172,8 @@ class ZeekEngine(object): for cidr in bl_cidrs: if IPAddress(c["ip_dst"]) in cidr[0]: c["alert_tiggered"] = True - self.alerts.append({"title": "Communication to {} under the CIDR {} which is tagged as {}.".format(c["resolution"], cidr[0], cidr[1].upper()), - "description": "The server {} is hosted under a network which is known to host malicious activities. Even if this behavior is not malicious by itself, ".format(c["resolution"]) - + "you need to check if other alerts are also mentioning this host. If you have some doubts, please " - + "search this host on the internet to see if its legit or not.", + self.alerts.append({"title": self.template["IOC-02"]["title"].format(c["resolution"], cidr[0], cidr[1].upper()), + "description": self.template["IOC-02"]["description"].format(c["resolution"]), "host": c["resolution"], "level": "Moderate", "id": "IOC-02"}) @@ -183,42 +182,37 @@ class ZeekEngine(object): if c["resolution"].endswith(domain[0]): if domain[1] != "tracker": c["alert_tiggered"] = True - self.alerts.append({"title": "A DNS request have been done to {} which is tagged as {}.".format(c["resolution"], domain[1].upper()), - "description": "The domain name {} seen in the capture has been explicitly tagged as malicious. This indicates that ".format(c["resolution"]) - + "your device is likely compromised and needs to be investigated deeply.", + self.alerts.append({"title": self.template["IOC-03"]["title"].format(c["resolution"], domain[1].upper()), + "description": self.template["IOC-03"]["description"].format(c["resolution"]), "host": c["resolution"], "level": "High", "id": "IOC-03"}) else: c["alert_tiggered"] = True - self.alerts.append({"title": "A DNS request have been done to {} which is tagged as {}.".format(c["resolution"], domain[1].upper()), - "description": "The domain name {} seen in the capture has been explicitly tagged as a Tracker. This ".format(c["resolution"]) - + "indicates that one of the active apps is geo-tracking your moves.", + self.alerts.append({"title": self.template["IOC-04"]["title"].format(c["resolution"], domain[1].upper()), + "description": self.template["IOC-04"]["description"].format(c["resolution"]), "host": c["resolution"], "level": "Moderate", - "id": "IOC-03"}) + "id": "IOC-04"}) # Check for blacklisted FreeDNS. for domain in bl_freedns: if c["resolution"].endswith("." + domain[0]): c["alert_tiggered"] = True - self.alerts.append({"title": "A DNS request have been done to the domain {} which is a Free DNS.".format(c["resolution"]), - "description": "The domain name {} is using a Free DNS service. This kind of service is commonly used by cybercriminals ".format(c["resolution"]) - + "or state-sponsored threat actors during their operations. It is very suspicious that an application running in background use this kind of service, please investigate.", + self.alerts.append({"title": self.template["IOC-05"]["title"].format(c["resolution"]), + "description": self.template["IOC-05"]["description"].format(c["resolution"]), "host": c["resolution"], "level": "Moderate", - "id": "IOC-04"}) + "id": "IOC-05"}) # Check for suspect tlds. for tld in bl_tlds: if c["resolution"].endswith(tld[0]): c["alert_tiggered"] = True - self.alerts.append({"title": "A DNS request have been done to the domain {} which contains a suspect TLD.".format(c["resolution"]), - "description": "The domain name {} is using a suspect Top Level Domain ({}). Even not malicious, this non-generic TLD is used regularly by cybercrime ".format(c["resolution"], tld[0]) - + "or state-sponsored operations. Please check this domain by searching it on an internet search engine. If other alerts are related to this " - + "host, please consider it as very suspicious.", + self.alerts.append({"title": self.template["IOC-06"]["title"].format(c["resolution"]), + "description": self.template["IOC-06"]["description"].format(c["resolution"], tld[0]), "host": c["resolution"], "level": "Low", - "id": "IOC-05"}) + "id": "IOC-06"}) # Check for use of suspect nameservers. try: @@ -230,13 +224,11 @@ class ZeekEngine(object): for ns in bl_nameservers: if name_servers[0].endswith(".{}.".format(ns[0])): c["alert_tiggered"] = True - self.alerts.append({"title": "The domain {} is using a suspect nameserver ({}).".format(c["resolution"], name_servers[0]), - "description": "The domain name {} is using a nameserver that has been explicitly tagged to be associated to malicious activities. ".format(c["resolution"]) - + "Many cybercriminals and state-sponsored threat actors are using this kind of registrars because they allow cryptocurrencies and anonymous payments. It" - + " is adviced to investigate on this domain and the associated running application by doing a forensic analysis of the phone.", + self.alerts.append({"title": self.template["IOC-07"]["title"].format(c["resolution"], name_servers[0]), + "description": self.template["IOC-07"]["description"].format(c["resolution"]), "host": c["resolution"], "level": "Moderate", - "id": "IOC-06"}) + "id": "IOC-07"}) def files_check(self, dir): """ @@ -268,12 +260,11 @@ class ZeekEngine(object): if f["sha1"] == cert[0]: host = self.resolve(f["ip_dst"]) c["alert_tiggered"] = True - self.alerts.append({"title": "A certificate associated to {} activities have been found in the communication to {}.".format(cert[1].upper(), host), - "description": "The certificate ({}) associated to {} has been explicitly tagged as malicious. This indicates that ".format(f["sha1"], host) - + "your device is likely compromised and need a forensic analysis.", + self.alerts.append({"title": self.template["IOC-08"]["title"].format(cert[1].upper(), host), + "description": self.template["IOC-08"]["description"].format(f["sha1"], host), "host": host, "level": "High", - "id": "IOC-07"}) + "id": "IOC-08"}) def ssl_check(self, dir): """ @@ -306,30 +297,24 @@ class ZeekEngine(object): # Check for non generic SSL port. if cert["port"] not in ssl_default_ports: c["alert_tiggered"] = True - self.alerts.append({"title": "SSL connection done on a non standard port ({}) to {}".format(cert["port"], host), - "description": "It is not common to see SSL connections issued from smartphones using non-standard ports. Even this can be totally legit," - + " we recommend to check the reputation of {}, by looking at its WHOIS record, the associated autonomus system, its creation date, and ".format(host) - + " by searching it the internet.", + self.alerts.append({"title": self.template["SSL-01"]["title"].format(cert["port"], host), + "description": self.template["SSL-01"]["description"].format(host), "host": host, "level": "Moderate", "id": "SSL-01"}) # Check Free SSL certificates. if cert["issuer"] in free_issuers: c["alert_tiggered"] = True - self.alerts.append({"title": "An SSL connection to {} is using a free certificate.".format(host), - "description": "Free certificates — such as Let's Encrypt — are wildly used by command and control servers associated to " - + "malicious implants or phishing web pages. We recommend to check the host associated to this certificate, " - + "by looking at the domain name, its creation date, or by checking its reputation on the internet.", + self.alerts.append({"title": self.template["SSL-02"]["title"].format(host), + "description": self.template["SSL-02"]["description"], "host": host, "level": "Moderate", "id": "SSL-02"}) # Check for self-signed certificates. if cert["validation_status"] == "self signed certificate in certificate chain": c["alert_tiggered"] = True - self.alerts.append({"title": "The certificate associated to {} is self-signed.".format(host), - "description": "The use of self-signed certificates is a common thing for attacker infrastructure. We recommend to check the host {} ".format(host) - + "which is associated to this certificate, by looking at the domain name (if any), its WHOIS record, its creation date, and " - + " by checking its reputation on the internet.", + self.alerts.append({"title": self.template["SSL-03"]["title"].format(host), + "description": self.template["SSL-03"]["description"].format(host), "host": host, "level": "Moderate", "id": "SSL-03"}) @@ -349,8 +334,8 @@ class ZeekEngine(object): for host, nb in hosts.items(): if nb >= get_config(("analysis", "max_alerts")): - self.alerts.append({"title": "Check alerts for {}".format(host), - "description": "Please, check the reputation of the host {}, this one seems to be malicious as it leveraged {} alerts during the session.".format(host, nb), + self.alerts.append({"title": self.template["ADV-01"]["title"].format(host), + "description": self.template["ADV-01"]["description"].format(host, nb), "host": host, "level": "High", "id": "ADV-01"}) diff --git a/analysis/locales/en.json b/analysis/locales/en.json new file mode 100644 index 0000000..94a2987 --- /dev/null +++ b/analysis/locales/en.json @@ -0,0 +1,108 @@ +{ + "alerts": { + "PROTO-01": { + "title": "{} communication going outside the local network to {}.", + "description": "The {} protocol is commonly used in internal networks. Please, verify if the host {} leveraged other alerts which may indicates a possible malicious behavior." + }, + "PROTO-02": { + "title": "{} connection to {} to a port over or equal to {}.", + "description": "{} connections have been seen to {} by using the port {}. The use of non-standard port can be sometimes associated to malicious activities. We recommend to check if this host has a good reputation by looking on other alerts and search it on the internet." + }, + "PROTO-03": { + "title": "HTTP communications have been done to the host {}", + "description": "Your device exchanged with the host {} by using HTTP, an unencrypted protocol. Even if this behavior is not malicious by itself, it is unusual to see HTTP communications issued from smartphone applications running in the background. Please check the host reputation by searching it on the internet." + }, + "PROTO-04": { + "title": "HTTP communications have been seen to the host {} on a non standard port ({}).", + "description": "Your device exchanged with the host {} by using HTTP, an unencrypted protocol on the port {}. This behavior is quite unusual. Please check the host reputation by searching it on the internet." + }, + "PROTO-05": { + "title": "The server {} hasn't been resolved by any DNS query during the session", + "description": "It means that the server {} is likely not resolved by any domain name or the resolution has already been cached by the device. If the host appears in other alerts, please check it." + }, + "IOC-01": { + "title": "A connection has been made to {} ({}) which is tagged as {}.", + "description": "The host {} has been explicitly blacklisted for malicious activities. Your device is likely compromised and needs to be investigated more deeply by IT security professionals." + }, + "IOC-02": { + "title": "Communication to {} under the CIDR {} which is tagged as {}.", + "description": "The server {} is hosted under a network which is known to host malicious activities. Even if this behavior is not malicious by itself, you need to check if other alerts are also mentioning this host. If you have some doubts, please search this host on the internet to see if its legit or not." + }, + "IOC-03": { + "title": "A DNS request have been done to {} which is tagged as {}.", + "description": "The domain name {} seen in the capture has been explicitly tagged as malicious. This indicates that your device is likely compromised and needs to be investigated deeply." + }, + "IOC-04": { + "title": "A DNS request have been done to {} which is tagged as {}.", + "description": "The domain name {} seen in the capture has been explicitly tagged as a Tracker. This indicates that one of the active apps is geo-tracking your moves." + }, + "IOC-05": { + "title": "A DNS request have been done to the domain {} which is a Free DNS.", + "description": "The domain name {} is using a Free DNS service. This kind of service is commonly used by cybercriminals or state-sponsored threat actors during their operations. It is very suspicious that an application running in background use this kind of service, please investigate." + }, + "IOC-06": { + "title": "A DNS request have been done to the domain {} which contains a suspect TLD.", + "description": "The domain name {} is using a suspect Top Level Domain ({}). Even not malicious, this non-generic TLD is used regularly by cybercrime or state-sponsored operations. Please check this domain by searching it on an internet search engine. If other alerts are related to this host, please consider it as very suspicious." + }, + "IOC-07": { + "title": "The domain {} is using a suspect nameserver ({}).", + "description": "The domain name {} is using a nameserver that has been explicitly tagged to be associated to malicious activities. Many cybercriminals and state-sponsored threat actors are using this kind of registrars because they allow cryptocurrencies and anonymous payments. It is adviced to investigate on this domain and the associated running application by doing a forensic analysis of the phone." + }, + "IOC-08": { + "title": "A certificate associated to {} activities have been found in the communication to {}.", + "description": "The certificate ({}) associated to {} has been explicitly tagged as malicious. This indicates that your device is likely compromised and need a forensic analysis." + }, + "SSL-01": { + "title": "SSL connection done on a non standard port ({}) to {}", + "description": "It is not common to see SSL connections issued from smartphones using non-standard ports. Even this can be totally legit, we recommend to check the reputation of {}, by looking at its WHOIS record, the associated autonomus system, its creation date, and by searching it the internet." + }, + "SSL-02": { + "title": "An SSL connection to {} is using a free certificate.", + "description": "Free certificates — such as Let's Encrypt — are wildly used by command and control servers associated to malicious implants or phishing web pages. We recommend to check the host associated to this certificate, by looking at the domain name, its creation date, or by checking its reputation on the internet." + }, + "SSL-03": { + "title": "The certificate associated to {} is self-signed.", + "description": "The use of self-signed certificates is a common thing for attacker infrastructure. We recommend to check the host {} which is associated to this certificate, by looking at the domain name (if any), its WHOIS record, its creation date, and by checking its reputation on the internet." + }, + "ADV-01": { + "title": "Check the alerts for {}", + "description": "Please, check the reputation of the host {}, this one seems to be malicious as it leveraged {} alerts during the session." + }, + "SNORT-01": { + "title": "Suricata rule tiggered: {}", + "description": "A network detection rule has been triggered. It's likely that your device has been compromised or have some suspect behaviour." + } + }, + "report": { + "numbers": [ + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine" + ], + "suspect_title": "Suspect communications", + "uncat_title": "Uncategorized communications", + "whitelist_title": "Whitelisted communications", + "protocol": "Protocol", + "domain": "Domain", + "dst_ip": "Dst IP address", + "dst_port": "Dst port number", + "device_name": "Device name", + "device_mac": "Device MAC address", + "report_generated_on": "Report generated on", + "capture_duration": "Capture duration", + "packets_number": "Number of packets", + "capture_sha1": "Capture SHA1", + "report_for_the_capture": "Report for the capture", + "report_footer": "This report has been autogenerated by a Tinycheck device. For any question, bug report or feedback, please contact tinycheck@kaspersky.com.", + "high_msg": "Your device seems to be compromised as you have {} high alert(s).", + "moderate_msg": "You have {} moderate alert(s), your device might be compromised. Please look at them carefully.", + "low_msg": "You have only {} low alert(s), don't hesitate to check them.", + "none_msg": "Everything looks fine, zero alerts. Don't hesitate to check the uncategorized communications, if any." + } +} \ No newline at end of file diff --git a/analysis/locales/fr.json b/analysis/locales/fr.json new file mode 100644 index 0000000..15be281 --- /dev/null +++ b/analysis/locales/fr.json @@ -0,0 +1,108 @@ +{ + "alerts": { + "PROTO-01": { + "title": "Une communication {} va à l'extérieur du réseau jusqu'à {}.", + "description": "Le protocole {} est généralement utilisé dans des réseaux internes. Vérifiez si le serveur {} a soulevé d'autres alertes. Ceci pourrait indiquer la présence d'une possible application malveillante." + }, + "PROTO-02": { + "title": "Connexion {} vers {} sur un port au dessus ou égal à {}.", + "description": "Des connexions {} vers {} ont été vues utilisant le port {}. L'utilisation d'un numéro de port non standard peut être associé à des activités malveillantes. Nous recommandons de vérifier si ce serveur possède une bonne réputation en regardant les autres alertes et en le cherchant sur Internet." + }, + "PROTO-03": { + "title": "Des communications HTTP ont été réalisées vers {}", + "description": "Votre appareil a communiqué avec le serveur {} en utilisant HTTP, un protocole non chiffré. Même si ce n'est pas malveillant en tant que tel, il est rare de voir des communications HTTP issues d'applications installées sur des smartphones exécutées en arrière plan. Il est conseillé de vérifier la réputation du serveur en le recherchant sur Internet." + }, + "PROTO-04": { + "title": "Des communications HTTP ont été réalisées vers {} sur un port non standard ({}).", + "description": "Votre appareil a communiqué vers le serveur {} en utilisant HTTP, un protocole non chiffré sur le port {}. Ce type de communication peut être signe d'une activité malveillante sur votre appareil car il est très rare qu'HTTP utilise ce port. Il est conseillé de vérifier la réputation du serveur en le recherchant sur Internet." + }, + "PROTO-05": { + "title": "Le serveur {} n'a pas été résolu par le protocole DNS durant la session", + "description": "Cela signifie que le serveur {} ne possède pas de nom de domaine associé ou que sa résolution a déjà été mise en cache par votre appareil. Si le serveur apparait dans d'autres alertes, merci de vérifier sa réputation." + }, + "IOC-01": { + "title": "Connexion vers {} ({}) qui est tagué en tant que {}.", + "description": "Le serveur {} est connu pour être associé à des activités malveillantes. Votre appareil est surement compromis et doit être investigué plus en détails par une équipe professionnelle." + }, + "IOC-02": { + "title": "Connexion vers {} appartenant au bloc réseau {} qui est tagué en tant que {}.", + "description": "Le serveur {} est hébergé dans un réseau qui est connu pour abriter des activités malveillantes. Même si ce n'est pas malveillant en tant que tel, vous devez regarder si d'autres alertes mentionnent ce serveur. Si vous avez certains doutes, recherchez sur Internet ce serveur pour savoir s'il semble être légitime ou non." + }, + "IOC-03": { + "title": "Requête DNS vers le domaine {} qui est tagué en tant que {}.", + "description": "Le serveur {} vers lequel communique votre appareil a été explicitement catégorisé en tant que malveillant. Votre appareil est sûrement compromis et doit être investigué plus en détails par une équipe professionnelle." + }, + "IOC-04": { + "title": "Requête DNS vers le domaine {} qui est tagué en tant que {}.", + "description": "Le nom de domaine {} vers lequel communique votre appareil a été explicitement catégorisé comme un tracker. Ceci indique d'une application active sur votre appareil est entrain de vous géo-localiser." + }, + "IOC-05": { + "title": "Requête DNS vers le domaine {} qui est un domaine gratuit.", + "description": "Le nom de domaine {} utilise un service de noms de domaine gratuits. Ce type de service est couramment utilisé par les cybercriminels ou des acteurs associés à des États au cours de leurs opérations d'espionnage. Il est très suspect qu'une application exécutée en arrière-plan utilise ce type de service, veuillez enquêter." + }, + "IOC-06": { + "title": "Requête DNS vers le domaine {} contenant une extension suspecte.", + "description": "Le nom de domaine {} utilise une extension suspecte ({}). Même si cela n'est pas malveillant en-soi, l'utilisation d'une extension non générique est l'apanage d'acteurs cybercriminels et étatiques durant leurs opérations. Veuillez vérifier la pertinance de ce domaine en le recherchant sur un moteur de recherche Internet. Si d'autres alertes sont liées à ce dernier, veuillez le considérer comme très suspect." + }, + "IOC-07": { + "title": "Le domaine {} utilise un serveur de noms suspect ({}).", + "description": "Le nom de domaine {} utilise un server de nom qui a été explicitement catégorisé comme associé à des activités malveillantes. Plusieurs cybercriminels et acteurs étatiques utilisent ce type de serveurs de noms car ils autorisent les paiements anonymes grâce aux cryptomonnaies. Il est conseillé d'investiguer sur ce domaine et l'application s'y connectant en réalisant une analyse post-mortem de l'appareil analysé." + }, + "IOC-08": { + "title": "Un certificat associé à des activités de {} a été vu lors de communications vers {}.", + "description": "Le certificat ({}) associé au serveur {} a été explicitement catégorisé comme malveillant. Votre appareil est sûrement compromis et doit être investigué plus en détails par une équipe professionnelle." + }, + "SSL-01": { + "title": "Connexion SSL utilisant un port non standard ({}) vers {}", + "description": "Il n'est pas commun de voir des connexions SSL issues de smartphones utiliser des ports non standards. Même si cela peut être totalement légitime, il est recommandé d'évaluer la réputation du serveur {}, en regardant son enregistrement WHOIS, son système autonome, sa date de création et en le recherchant sur Internet." + }, + "SSL-02": { + "title": "Une connexion SSL vers {} utilise un certificat gratuit.", + "description": "Les certificats gratuits — tels que Let's Encrypt - sont largement utilisés par des serveurs de commande et de contrôle associés à des implants malveillants ou à des pages Web de phishing. Nous vous recommandons de vérifier le serveur associé à ce certificat, en regardant le nom de domaine, sa date de création, ou en vérifiant sa réputation sur Internet." + }, + "SSL-03": { + "title": "Le certificat associé à {} est auto-signé.", + "description": "L'utilisation de certificats auto-signés est une chose courante pour des infrastructures d'attaque associées à des activités malveillantes. Nous vous recommandons de vérifier le serveur {} qui est associé à ce certificat, en regardant le nom de domaine (le cas échéant), son enregistrement WHOIS, sa date de création, et en vérifiant sa réputation sur Internet." + }, + "ADV-01": { + "title": "Vérifiez les alertes liées au serveur {}", + "description": "Merci de vérifier la réputation et les alertes liées au serveur {}, ce dernier semble malveillant, ayant engendré {} alertes durant la session de capture." + }, + "SNORT-01": { + "title": "Règle suricata déclanchée : {}", + "description": "Une règle de détection de réseau a été déclenchée. Il est probable que votre appareil ait été compromis ou présente un comportement suspect." + } + }, + "report": { + "numbers": [ + "une", + "deux", + "trois", + "quatre", + "cinq", + "six", + "sept", + "huit", + "neuf" + ], + "suspect_title": "Communications suspectes", + "uncat_title": "Communications non catégorisées", + "whitelist_title": "Communications légitimes", + "protocol": "Protocole", + "domain": "Domaine", + "dst_ip": "Adresse IP", + "dst_port": "Port", + "device_name": "Nom de l'appareil", + "device_mac": "Adresse MAC de l'appareil", + "report_generated_on": "Rapport généré le ", + "capture_duration": "Durée de la capture", + "packets_number": "Nombre de paquets", + "capture_sha1": "SHA1 de la capture", + "report_for_the_capture": "Rapport pour la capture", + "report_footer": "Ce rapport a été automatiquement généré par une instance de TinyCheck. Pour toute question et retours, n'hésitez pas à contacter tinycheck@kaspersky.com.", + "high_msg": "Votre appareil semble être compromis car vous avez {} alerte(s) élevée(s).", + "moderate_msg": "Vous avez {} alerte(s) modérée(s), votre appareil peut être compromis. Regardez ces alertes en détail.", + "low_msg": "Vous avez uniquement {} alerte(s) faibles, n'hésitez pas à les consulter.", + "none_msg": "Toute semble normal, vous avez aucune alerte. Cependant, n'hésitez pas à regarder les communications non catégorisées." + } +} \ No newline at end of file diff --git a/app/frontend/package-lock.json b/app/frontend/package-lock.json index ebba075..1a12fee 100644 --- a/app/frontend/package-lock.json +++ b/app/frontend/package-lock.json @@ -4638,6 +4638,24 @@ "domelementtype": "1" } }, + "dot-object": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/dot-object/-/dot-object-2.1.4.tgz", + "integrity": "sha512-7FXnyyCLFawNYJ+NhkqyP9Wd2yzuo+7n9pGiYpkmXCTYa8Ci2U0eUNDVg5OuO5Pm6aFXI2SWN8/N/w7SJWu1WA==", + "dev": true, + "requires": { + "commander": "^4.0.0", + "glob": "^7.1.5" + }, + "dependencies": { + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + } + } + }, "dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -6877,6 +6895,12 @@ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, + "is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=", + "dev": true + }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -11315,6 +11339,32 @@ "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==", "dev": true }, + "vue-i18n": { + "version": "8.22.4", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.22.4.tgz", + "integrity": "sha512-XLI5s0AdqMP2Lf4I4CmdmOq8kjb5DDFGR77wAuxCfpEuYSfhTRyyx6MetgZMiL6Lxa0DasjBOiOcciU3NkL3/Q==" + }, + "vue-i18n-extract": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/vue-i18n-extract/-/vue-i18n-extract-1.1.10.tgz", + "integrity": "sha512-DYqcjrAm4L95Ftz8AVIzUyrHCBQV326rbOru6B3W2vXHIzUUuJwwFgyVPE+d7YTeHD189eoOkRu0aL6EzeMWZQ==", + "dev": true, + "requires": { + "commander": "^6.1.0", + "dot-object": "^2.1.4", + "glob": "^7.1.6", + "is-valid-glob": "^1.0.0", + "js-yaml": "^3.14.0" + }, + "dependencies": { + "commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true + } + } + }, "vue-loader": { "version": "15.9.3", "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.9.3.tgz", diff --git a/app/frontend/package.json b/app/frontend/package.json index 073ee82..5655dd9 100644 --- a/app/frontend/package.json +++ b/app/frontend/package.json @@ -5,7 +5,8 @@ "scripts": { "serve": "vue-cli-service serve --copy --port=4202", "build": "vue-cli-service build", - "lint": "vue-cli-service lint" + "lint": "vue-cli-service lint", + "vue-i18n-extract": "vue-i18n-extract report -a -v './src/**/*.?(js|vue)' -l './src/locales/*.?(json|yaml|yml)'" }, "dependencies": { "@fnando/sparkline": "^0.3.10", @@ -15,6 +16,7 @@ "sass-loader": "^10.0.4", "simple-keyboard": "^2.30.25", "vue": "^2.6.12", + "vue-i18n": "^8.22.4", "vue-router": "^3.4.3" }, "devDependencies": { @@ -24,6 +26,7 @@ "babel-eslint": "^10.1.0", "eslint": "^7.9.0", "eslint-plugin-vue": "^6.2.2", + "vue-i18n-extract": "1.1.10", "vue-template-compiler": "^2.6.12" }, "eslintConfig": { diff --git a/app/frontend/src/App.vue b/app/frontend/src/App.vue index de6927f..b8668ba 100644 --- a/app/frontend/src/App.vue +++ b/app/frontend/src/App.vue @@ -28,13 +28,38 @@ </style> <script> - document.title = 'TinyCheck Frontend' + import axios from 'axios' import Controls from "@/components/Controls.vue" + + document.title = 'TinyCheck Frontend' export default { name: 'app', components: { Controls + }, + methods: { + set_lang: function() { + if (window.config.user_lang) { + var lang = window.config.user_lang + if (Object.keys(this.$i18n.messages).includes(lang)) { + this.$i18n.locale = lang + document.querySelector('html').setAttribute('lang', lang) + } + } + }, + get_config: function() { + axios.get('/api/misc/config', { timeout: 60000 }) + .then(response => { + window.config = response.data + this.set_lang(); + }) + .catch(error => { console.log(error) }); + } + }, + created: function() { + window.config = {} + this.get_config(); } } </script> diff --git a/app/frontend/src/locales/en.json b/app/frontend/src/locales/en.json new file mode 100644 index 0000000..e7786bf --- /dev/null +++ b/app/frontend/src/locales/en.json @@ -0,0 +1,81 @@ +{ + "home": { + "welcome_msg": "Welcome to TinyCheck.", + "help_msg": "We are going to help you to check your device.", + "start_btn": "Let's start!" + }, + "analysis": { + "question": "Do you want to analyze the captured communications?", + "no_btn": "No, just save them", + "yes_btn": "Yes, let's do it", + "please_wait_msg": "Please wait during the analysis...", + "some_time_msg": "Yes, it can take some time..." + }, + "capture": { + "intercept_coms_msg": "Intercepting the communications of ", + "stop_btn": "Stop the capture" + }, + "generate-ap": { + "network_name": "Network name", + "network_password": "Network password", + "tap_msg": "Tap the white frame to generate a new network.", + "generate_ap_msg": "We generate an ephemeral network for you.", + "error_msg1": "Unfortunately, we got some issues <br />during the AP creation.", + "error_msg2": "Please verify that you've two WiFi interfaces on your device<br /> and try again by restarting it.", + "restart_btn": "Restart the device" + }, + "report": { + "show_full_report": "Show the full report", + "start_new_capture": "Start a new capture", + "save_capture": "Save the capture", + "numbers": [ + "zero", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "ten", + "eleven" + ], + "stalkerware_msg": "Your device is compromised by<br />a Stalkerware, please check the report.", + "location_msg": "An application is sharing your<br /> current geolocation with a third party.", + "fine_msg": "Everything looks fine, zero alerts.", + "high_msg": "You have {nb} high alert,<br />your device seems to be compromised.", + "moderate_msg": "You have {nb} moderate alerts,<br />your device might be compromised.", + "low_msg": "You have only {nb} low alerts,<br /> don't hesitate to check them.", + "save_report": "Save the report", + "report_of": "Report of", + "ip_address": "IP address", + "mac_address": "MAC address", + "high": "high", + "moderate": "moderate", + "low": "low" + }, + "wifi-select": { + "already_connected_question": "You seem to be already connected to a network.<br />Do you want to use the current connection?", + "no_btn": "No, use another", + "yes_btn": "Yes, use it.", + "wifi_name": "Wi-Fi name", + "refresh_btn": "Refresh networks list", + "not_connected": "You seem to not be connected to Internet.", + "please_config": "Please configure the Wi-Fi connection.", + "lets_do_btn": "Ok, let's do that.", + "wifi_connected": "Wi-Fi connected", + "wifi_not_connected": "Wi-Fi not connected. Please retry.", + "tap_keyboard": "Tap on the virtual keyboard to start", + "wifi_password": "Wi-Fi password", + "connect_to_it": "Connect to it" + }, + "save-capture": { + "please_connect": "Please connect a USB key to save your capture.", + "we_are_saving": "We are saving your capture.", + "tap_msg": "You can tap the USB key to start a new capture.", + "capture_download": "The capture download is going to start...", + "start_capture_btn": "Start another capture" + } +} \ No newline at end of file diff --git a/app/frontend/src/locales/fr.json b/app/frontend/src/locales/fr.json new file mode 100644 index 0000000..ef7b373 --- /dev/null +++ b/app/frontend/src/locales/fr.json @@ -0,0 +1,81 @@ +{ + "home": { + "welcome_msg": "Bienvenue sur TinyCheck.", + "help_msg": "Nous allons vous accompagner pour analyser votre appareil.", + "start_btn": "Allons-y !" + }, + "analysis": { + "question": "Voulez-vous analyser les communications capturées ?", + "no_btn": "Non, enregistrez-les.", + "yes_btn": "Oui, allons-y !", + "please_wait_msg": "Merci d'attendre pendant l'analyse...", + "some_time_msg": "Oui, cela peut prendre du temps..." + }, + "capture": { + "intercept_coms_msg": "Interception des communications de ", + "stop_btn": "Arrêter la capture" + }, + "generate-ap": { + "network_name": "Nom du réseau Wi-Fi", + "network_password": "Mot de passe", + "tap_msg": "Appuyez sur le cadre blanc pour générer un nouveau réseau.", + "generate_ap_msg": "Nous générons un réseau Wi-Fi éphémère.", + "error_msg1": "Malheureusement, nous rencontrons des problèmes <br />lors de la création du point d'accès.", + "error_msg2": "Veuillez vérifier que vous disposez de <br /> deux interfaces WiFi sur votre appareil.", + "restart_btn": "Restart the device" + }, + "report": { + "show_full_report": "Lire le rapport complet", + "start_new_capture": "Nouvelle capture", + "save_capture": "Sauvegarder la capture", + "numbers": [ + "zéro", + "une", + "deux", + "trois", + "quatre", + "cinq", + "six", + "sept", + "huit", + "neuf", + "dix", + "onze" + ], + "stalkerware_msg": "Votre smartphone est compromis<br /> par un Stalkerware, lisez le rapport.", + "location_msg": "Une application dévoile votre<br /> géo-localisation actelle.", + "fine_msg": "Aucun flux suspect ou malveillant détecté,<br />vous avez aucune alerte.", + "high_msg": "Vous avez {nb} alerte.s élevée.s,<br />votre appareil semble être compromis.", + "moderate_msg": "Vous avez {nb} alerte.s moyenne.s,<br />votre appareil est peut-être compromis.", + "low_msg": "Vous avez uniquement {nb} alerte.s basse.s<br /> n'hésitez pas à les consulter.", + "save_report": "Archiver le rapport", + "report_of": "Rapport de", + "ip_address": "Adresse IP :", + "mac_address": "Adresse MAC :", + "high": "elevee", + "moderate": "moyenne", + "low": "base" + }, + "wifi-select": { + "already_connected_question": "Vous semblez être connecté à un réseau.<br />Voulez-vous utiliser cette connexion ?", + "no_btn": "Non, une autre", + "yes_btn": "Oui, utilisez celle-là", + "wifi_name": "Nom du réseau", + "refresh_btn": "Rafraichir la liste des réseaux", + "not_connected": "Vous semblez ne pas être connecté à Internet.", + "please_config": "Configurez la connexion Wi-Fi.", + "lets_do_btn": "Oui, allons-y", + "wifi_connected": "Wi-Fi connecté", + "wifi_not_connected": "Wi-Fi non connecté. Ressayez.", + "tap_keyboard": "Appuez sur cet espace pour écrire", + "wifi_password": "Mot de passe", + "connect_to_it": "Se connecter" + }, + "save-capture": { + "please_connect": "Connectez une clé USB pour enregistrer votre capture.", + "we_are_saving": "Nous enregistrons votre capture.", + "tap_msg": "Vous pouvez appuyer sur l'animation pour lancer une nouvelle capture.", + "capture_download": "Le téléchargement de la capture va se lancer...", + "start_capture_btn": "Lancer une nouvelle capture" + } +} \ No newline at end of file diff --git a/app/frontend/src/main.js b/app/frontend/src/main.js index 3c99e4e..7b84c01 100644 --- a/app/frontend/src/main.js +++ b/app/frontend/src/main.js @@ -1,10 +1,13 @@ import Vue from 'vue' import App from './App.vue' import router from './router' +import { i18n } from '@/plugins/i18n' + Vue.config.productionTip = true Vue.config.devtools = true new Vue({ router, + i18n, render: h => h(App) }).$mount('#app') \ No newline at end of file diff --git a/app/frontend/src/plugins/i18n.js b/app/frontend/src/plugins/i18n.js new file mode 100644 index 0000000..d48a14b --- /dev/null +++ b/app/frontend/src/plugins/i18n.js @@ -0,0 +1,13 @@ +import Vue from 'vue' +import VueI18n from 'vue-i18n' + +Vue.use(VueI18n) + +export const i18n = new VueI18n({ + locale: 'en', + fallbackLocale: 'en', + messages: { + 'en': require('@/locales/en.json'), + 'fr': require('@/locales/fr.json') + } +}) \ No newline at end of file diff --git a/app/frontend/src/router/index.js b/app/frontend/src/router/index.js index 642c540..bcf457d 100644 --- a/app/frontend/src/router/index.js +++ b/app/frontend/src/router/index.js @@ -41,12 +41,14 @@ const routes = [ component: () => import('../views/save-capture.vue'), props: true }, - { path: '/analysis', + { + path: '/analysis', name: 'analysis', component: () => import('../views/analysis.vue'), props: true }, - { path: '/report', + { + path: '/report', name: 'report', component: () => import('../views/report.vue'), props: true diff --git a/app/frontend/src/views/analysis.vue b/app/frontend/src/views/analysis.vue index d0e68d9..c852ed7 100644 --- a/app/frontend/src/views/analysis.vue +++ b/app/frontend/src/views/analysis.vue @@ -1,15 +1,15 @@ <template> <div class="center"> <div v-if="question"> - <p>Do you want to analyze the captured communications?</p> + <p>{{ $t("analysis.question") }}</p> <div class="empty-action"> - <button class="btn" v-on:click="save_capture()">No, just save them</button> <button class="btn btn-primary" v-on:click="start_analysis()">Yes, let's do it</button> + <button class="btn" v-on:click="save_capture()">{{ $t("analysis.no_btn") }}</button> <button class="btn btn-primary" v-on:click="start_analysis()">{{ $t("analysis.yes_btn") }}</button> </div> </div> <div v-else-if="running"> <img src="@/assets/loading.svg"/> - <p class="legend" v-if="!long_waiting">Please wait during the analysis...</p> - <p class="legend fade-in" v-if="long_waiting">Yes, it can take some time...</p> + <p class="legend" v-if="!long_waiting">{{ $t("analysis.please_wait_msg") }}</p> + <p class="legend fade-in" v-if="long_waiting">{{ $t("analysis.some_time_msg") }}</p> </div> </div> </template> @@ -25,7 +25,8 @@ export default { question: true, running: false, check_alerts: false, - long_waiting: false + long_waiting: false, + translation: {} } }, props: { @@ -38,7 +39,7 @@ export default { setTimeout(function () { this.long_waiting = true }.bind(this), 15000); axios.get(`/api/analysis/start/${this.capture_token}`, { timeout: 60000 }) .then(response => { - if(response.data.message == "Analysis started") + if(response.data.message == 'Analysis started') this.check_alerts = setInterval(() => { this.get_alerts(); }, 500); }) .catch(error => { @@ -48,13 +49,14 @@ export default { get_alerts: function() { axios.get(`/api/analysis/report/${this.capture_token}`, { timeout: 60000 }) .then(response => { - if(response.data.message != "No report yet"){ + if(response.data.message != 'No report yet'){ clearInterval(this.check_alerts); this.long_waiting = false; this.running = false; - router.replace({ name: 'report', params: { alerts : response.data.alerts, - device : response.data.device, - capture_token : this.capture_token } }); + router.replace({ name: 'report', + params: { alerts : response.data.alerts, + device : response.data.device, + capture_token : this.capture_token } }); } }) .catch(error => { @@ -63,7 +65,8 @@ export default { }, save_capture: function() { var capture_token = this.capture_token - router.replace({ name: 'save-capture', params: { capture_token: capture_token } }); + router.replace({ name: 'save-capture', + params: { capture_token: capture_token } }); } } } diff --git a/app/frontend/src/views/capture.vue b/app/frontend/src/views/capture.vue index 10d0f8d..4323d10 100644 --- a/app/frontend/src/views/capture.vue +++ b/app/frontend/src/views/capture.vue @@ -4,9 +4,9 @@ <div class="center"> <div class="footer"> <h3 class="timer">{{timer_hours}}:{{timer_minutes}}:{{timer_seconds}}</h3> - <p>Intercepting the communications of {{device_name}}.</p> + <p>{{$t("capture.intercept_coms_msg")}} {{device_name}}.</p> <div class="empty-action"> - <button class="btn" :class="[ loading ? 'loading' : 'btn-primary', ]" v-on:click="stop_capture()">Stop the capture</button> + <button class="btn" :class="[ loading ? 'loading' : 'btn-primary', ]" v-on:click="stop_capture()">{{$t("capture.stop_btn")}}</button> </div> </div> </div> @@ -29,7 +29,8 @@ export default { loading: false, stats_interval: false, chrono_interval: false, - sparklines: false + sparklines: false, + translation: {} } }, props: { @@ -42,16 +43,16 @@ export default { }, stop_capture: function() { this.loading = true - axios.get(`/api/network/ap/stop`, { timeout: 30000 }) - axios.get(`/api/capture/stop`, { timeout: 30000 }) + axios.get('/api/network/ap/stop', { timeout: 30000 }) + axios.get('/api/capture/stop', { timeout: 30000 }) .then(response => (this.handle_finish(response.data))) }, get_stats: function() { - axios.get(`/api/capture/stats`, { timeout: 30000 }) + axios.get('/api/capture/stats', { timeout: 30000 }) .then(response => (this.handle_stats(response.data))) }, handle_stats: function(data) { - if (data.packets.length) sparkline(document.querySelector("#sparkline"), data.packets); + if (data.packets.length) sparkline(document.querySelector('#sparkline'), data.packets); }, handle_finish: function(data) { clearInterval(this.chrono_interval); @@ -65,33 +66,33 @@ export default { chrono: function() { var time = Date.now() - this.capture_start this.timer_hours = Math.floor(time / (60 * 60 * 1000)); - this.timer_hours = (this.timer_hours < 10) ? "0" + this.timer_hours : this.timer_hours + this.timer_hours = (this.timer_hours < 10) ? '0' + this.timer_hours : this.timer_hours time = time % (60 * 60 * 1000); this.timer_minutes = Math.floor(time / (60 * 1000)); - this.timer_minutes = (this.timer_minutes < 10) ? "0" + this.timer_minutes : this.timer_minutes + this.timer_minutes = (this.timer_minutes < 10) ? '0' + this.timer_minutes : this.timer_minutes time = time % (60 * 1000); this.timer_seconds = Math.floor(time / 1000); - this.timer_seconds = (this.timer_seconds < 10) ? "0" + this.timer_seconds : this.timer_seconds + this.timer_seconds = (this.timer_seconds < 10) ? '0' + this.timer_seconds : this.timer_seconds }, setup_sparklines: function() { - axios.get(`/api/misc/config`, { timeout: 60000 }) + axios.get('/api/misc/config', { timeout: 60000 }) .then(response => { if(response.data.sparklines){ this.sparklines = true - this.sparkwidth = window.screen.width + "px"; - this.sparkheight = Math.trunc(window.screen.height / 5) + "px"; + this.sparkwidth = window.screen.width + 'px'; + this.sparkheight = Math.trunc(window.screen.height / 5) + 'px'; this.stats_interval = setInterval(() => { this.get_stats(); }, 500); } }) .catch(error => { console.log(error) - }); - } + }); + }, }, created: function() { // Get the config for the sparklines. this.setup_sparklines() - + // Start the chrono and get the first stats. this.capture_start = Date.now() this.set_chrono(); diff --git a/app/frontend/src/views/generate-ap.vue b/app/frontend/src/views/generate-ap.vue index 39d1fb0..663238c 100644 --- a/app/frontend/src/views/generate-ap.vue +++ b/app/frontend/src/views/generate-ap.vue @@ -9,29 +9,30 @@ </div> <div class="divider-vert white-bg" data-content="OR"></div> <div class="column col-5"><br /> - <span class="light-grey">Network name: </span><br /> + <span class="light-grey">{{ $t("generate-ap.network_name") }} </span><br /> <h4>{{ ssid_name }}</h4> - <span class="light-grey">Network password: </span><br /> + <span class="light-grey">{{ $t("generate-ap.network_password") }} </span><br /> <h4>{{ ssid_password }}</h4> </div> </div> </div> <br /><br /><br /><br /> <br /><br /><br /><br /><br /><br /> <!-- Requite a CSS MEME for that shit :) --> - <span class="legend">Tap the white frame to generate a new network.</span> + <span class="legend">{{ $t("generate-ap.tap_msg") }}</span> </div> <div v-else> <img src="@/assets/loading.svg"/> - <p class="legend">We generate an ephemeral network for you.</p> + <p class="legend">{{ $t("generate-ap.generate_ap_msg") }}</p> </div> </div> <div v-else> <p> - <strong>Unfortunately, we got some issues <br />during the AP creation.</strong> + <strong v-html="$t('generate-ap.error_msg1')"></strong> <br /><br /> - Please verify that you've two WiFi interfaces on your device<br /> and try again by restarting it.<br /><br /> + <span v-html="$t('generate-ap.error_msg2')"></span> + <br /><br /> </p> - <button v-if="reboot_option" class="btn" v-on:click="reboot()">Restart the device</button> + <button v-if="window.config.reboot_option" class="btn" v-on:click="reboot()">{{ $t("generate-ap.restart_btn") }}</button> </div> </div> @@ -54,14 +55,15 @@ export default { interval: false, error: false, reboot_option: false, - attempts: 3 + attempts: 3, + translation: {} } }, methods: { generate_ap: function() { clearInterval(this.interval); this.ssid_name = false - axios.get(`/api/network/ap/start`, { timeout: 30000 }) + axios.get('/api/network/ap/start', { timeout: 30000 }) .then(response => (this.show_ap(response.data))) }, show_ap: function(data) { @@ -80,11 +82,11 @@ export default { } }, start_capture: function() { - axios.get(`/api/capture/start`, { timeout: 30000 }) + axios.get('/api/capture/start', { timeout: 30000 }) .then(response => (this.get_capture_token(response.data))) }, reboot: function() { - axios.get(`/api/misc/reboot`, { timeout: 30000 }) + axios.get('/api/misc/reboot', { timeout: 30000 }) .then(response => { console.log(response)}) }, get_capture_token: function(data) { @@ -115,19 +117,9 @@ export default { } }); } - }, - load_config: function() { - axios.get(`/api/misc/config`, { timeout: 60000 }) - .then(response => { - this.reboot_option = response.data.reboot_option - }) - .catch(error => { - console.log(error) - }); - }, + } }, created: function() { - this.load_config() this.generate_ap(); } } diff --git a/app/frontend/src/views/home.vue b/app/frontend/src/views/home.vue index 5714ebd..6f7b52a 100644 --- a/app/frontend/src/views/home.vue +++ b/app/frontend/src/views/home.vue @@ -1,8 +1,8 @@ <template> <div class="center"> - <h3 class="lobster">Welcome to TinyCheck.</h3> - <p>We are going to help you to check your device.</p> - <button class="btn btn-primary" v-on:click="next()">Let's start!</button> + <h3 class="lobster">{{ $t("home.welcome_msg") }}</h3> + <p>{{ $t("home.help_msg") }}</p> + <button class="btn btn-primary" v-on:click="next()">{{ $t("home.start_btn") }}</button> </div> </template> @@ -11,22 +11,26 @@ import router from '../router' export default { name: 'home', - props: { saved_ssid: String, iface_out: String, list_ssids: Array, internet: Boolean }, + props: { saved_ssid: String, list_ssids: Array, internet: Boolean }, + data() { + return { + translation: {}, + } + }, methods: { next: function() { var saved_ssid = this.saved_ssid var list_ssids = this.list_ssids var internet = this.internet - console.log(this.iface_out) - if (this.iface_out.charAt(0) == "e"){ + if (window.config.iface_out.charAt(0) == "e"){ router.push({ name: 'generate-ap' }); } else { router.push({ name: 'wifi-select', params: { saved_ssid: saved_ssid, list_ssids: list_ssids, - internet:internet } }); + internet: internet } }); } - } + } } } </script> diff --git a/app/frontend/src/views/report.vue b/app/frontend/src/views/report.vue index b9b6708..5bfe8ff 100644 --- a/app/frontend/src/views/report.vue +++ b/app/frontend/src/views/report.vue @@ -3,69 +3,69 @@ <div v-if="results"> <div v-if="grep_keyword('STALKERWARE', 'high')" class="high-wrapper"> <div class="center"> - <h1 class="warning-title">Your device is compromised by<br />a Stalkerware, please check the report.</h1> - <button class="btn btn-report-low-light" v-on:click="new_capture()">Start a new capture</button> - <button class="btn btn-report-high" @click="show_report=true;results=false;">Show the full report</button> + <h1 class="warning-title" v-html="$t('report.stalkerware_msg')"></h1> + <button class="btn btn-report-low-light" v-on:click="new_capture()">{{ $t("report.start_new_capture") }}</button> + <button class="btn btn-report-high" @click="show_report=true;results=false;">{{ $t("report.show_full_report") }}</button> </div> </div> <div v-else-if="alerts.high.length >= 1" class="high-wrapper"> <div class="center"> - <h1 class="warning-title">You have {{ nb_translate(alerts.high.length) }} high alert,<br />your device seems to be compromised.</h1> - <button class="btn btn-report-low-light" v-on:click="new_capture()">Start a new capture</button> - <button class="btn btn-report-high" @click="show_report=true;results=false;">Show the full report</button> + <h1 class="warning-title" v-html="$t('report.high_msg', { nb: $i18n.messages[$i18n.locale].report.numbers[alerts.high.length] })"></h1> + <button class="btn btn-report-low-light" v-on:click="new_capture()">{{ $t("report.start_new_capture") }}</button> + <button class="btn btn-report-high" @click="show_report=true;results=false;">{{ $t("report.show_full_report") }}</button> </div> </div> <div v-else-if="grep_keyword('TRACKER', 'moderate')" class="med-wrapper"> <div class="center"> - <h1 class="warning-title">An application is sharing your<br /> current geolocation with a third party.</h1> - <button class="btn btn-report-low-light" v-on:click="new_capture()">Start a new capture</button> - <button class="btn btn-report-moderate" @click="show_report=true;results=false;">Show the full report</button> + <h1 class="warning-title" v-html="$t('report.location_msg')"></h1> + <button class="btn btn-report-low-light" v-on:click="new_capture()">{{ $t("report.start_new_capture") }}</button> + <button class="btn btn-report-moderate" @click="show_report=true;results=false;">{{ $t("report.show_full_report") }}</button> </div> </div> <div v-else-if="alerts.moderate.length >= 1" class="med-wrapper"> <div class="center"> - <h1 class="warning-title">You have {{ nb_translate(alerts.moderate.length) }} moderate alerts,<br />your device might be compromised.</h1> - <button class="btn btn-report-low-light" v-on:click="new_capture()">Start a new capture</button> - <button class="btn btn-report-moderate" @click="show_report=true;results=false;">Show the full report</button> + <h1 class="warning-title" v-html="$t('report.moderate_msg', { nb: $i18n.messages[$i18n.locale].report.numbers[alerts.moderate.length] })"></h1> + <button class="btn btn-report-low-light" v-on:click="new_capture()">{{ $t("report.start_new_capture") }}</button> + <button class="btn btn-report-moderate" @click="show_report=true;results=false;">{{ $t("report.show_full_report") }}</button> </div> </div> <div v-else-if="alerts.low.length >= 1" class="low-wrapper"> <div class="center"> - <h1 class="warning-title">You have only {{ nb_translate(alerts.moderate.low) }} low alerts,<br /> don't hesitate to check them.</h1> - <button class="btn btn-report-low-light" v-on:click="new_capture()">Start a new capture</button> - <button class="btn btn-report-low" @click="show_report=true;results=false;">Show the full report</button> + <h1 class="warning-title" v-html="$t('report.low_msg', { nb: $i18n.messages[$i18n.locale].report.numbers[alerts.low.length] })"></h1> + <button class="btn btn-report-low-light" v-on:click="new_capture()">{{ $t("report.start_new_capture") }}</button> + <button class="btn btn-report-low" @click="show_report=true;results=false;">{{ $t("report.show_full_report") }}</button> </div> </div> <div v-else class="none-wrapper"> <div class="center"> - <h1 class="warning-title">Everything looks fine, zero alerts.</h1> - <button class="btn btn-report-low-light" v-on:click="save_capture()">Save the capture</button> - <button class="btn btn-report-low" v-on:click="new_capture()">Start a new capture</button> + <h1 class="warning-title">{{ $t("report.fine_msg") }}</h1> + <button class="btn btn-report-low-light" v-on:click="save_capture()">{{ $t("report.save_capture") }}</button> + <button class="btn btn-report-low" v-on:click="new_capture()">{{ $t("report.start_new_capture") }}</button> </div> </div> </div> <div v-else-if="show_report" class="report-wrapper"> <div class="device-ctx"> - <h3 style="margin: 0;">Report for {{device.name}}</h3> - IP Address: {{device.ip_address}}<br />Mac Address: {{device.mac_address}} + <h3 style="margin: 0;">{{ $t("report.report_of") }} {{device.name}}</h3> + {{ $t("report.ip_address") }} {{device.ip_address}}<br />{{ $t("report.mac_address") }} {{device.mac_address}} </div> <ul class="alerts"> <li class="alert" v-for="alert in alerts.high" :key="alert.message"> - <span class="high-label">High</span><span class="alert-id">{{ alert.id }}</span> + <span class="high-label">{{ $t("report.high") }}</span><span class="alert-id">{{ alert.id }}</span> <div class="alert-body"> <span class="title">{{ alert.title }}</span> <p class="description">{{ alert.description }}</p> </div> </li> <li class="alert" v-for="alert in alerts.moderate" :key="alert.message"> - <span class="moderate-label">Moderate</span><span class="alert-id">{{ alert.id }}</span> + <span class="moderate-label">{{ $t("report.moderate") }}</span><span class="alert-id">{{ alert.id }}</span> <div class="alert-body"> <span class="title">{{ alert.title }}</span> <p class="description">{{ alert.description }}</p> </div> </li> <li class="alert" v-for="alert in alerts.low" :key="alert.message"> - <span class="low-label">Low</span><span class="alert-id">{{ alert.id }}</span> + <span class="low-label">{{ $t("report.low") }}</span><span class="alert-id">{{ alert.id }}</span> <div class="alert-body"> <span class="title">{{ alert.title }}</span> <p class="description">{{ alert.description }}</p> @@ -74,11 +74,11 @@ </ul> <div class="columns" id="controls-analysis"> <div class="column col-5"> - <button class="btn width-100" @click="$router.push('generate-ap')">Start a capture</button> + <button class="btn width-100" @click="$router.push('generate-ap')">{{ $t("report.start_new_capture") }}</button> </div> <div class="divider-vert column col-2" data-content="OR"></div> <div class="column col-5"> - <button class="btn btn btn-primary width-100" v-on:click="save_capture()">Save the report</button> + <button class="btn btn btn-primary width-100" v-on:click="save_capture()">{{ $t("report.save_report") }}</button> </div> </div> </div> @@ -100,6 +100,7 @@ export default { data() { return { results: true, + translation: {} } }, props: { @@ -115,15 +116,6 @@ export default { new_capture: function() { router.push({ name: 'generate-ap' }) }, - nb_translate: function(x) { - var nbs = ['zero','one','two','three','four', 'five','six','seven','eight','nine', 'ten', 'eleven'] - try { - return nbs[x]; - } catch (error) - { - return x; - } - }, grep_keyword: function(kw, level){ try { if(this.alerts[level].length){ diff --git a/app/frontend/src/views/save-capture.vue b/app/frontend/src/views/save-capture.vue index fd989dd..d94013d 100644 --- a/app/frontend/src/views/save-capture.vue +++ b/app/frontend/src/views/save-capture.vue @@ -6,14 +6,14 @@ <div class="icon-usb"></div> <div class="icon-usb-plug"></div> </div> - <p class="legend" v-if="!saved && !usb"><br />Please connect a USB key to save your capture.</p> - <p class="legend" v-if="!saved && usb"><br />We are saving your capture.</p> - <p class="legend" v-if="saved"><br />You can tap the USB key to start a new capture.</p> + <p class="legend" v-if="!saved && !usb"><br />{{ $t("save-capture.please_connect") }}</p> + <p class="legend" v-if="!saved && usb"><br />{{ $t("save-capture.we_are_saving") }}</p> + <p class="legend" v-if="saved"><br />{{ $t("save-capture.tap_msg") }}</p> </div> <div class="center" v-else-if="!save_usb && init"> <div> - <p class="legend">The capture download is going to start...<br /><br /><br /></p> - <button class="btn btn-primary" v-on:click="new_capture()">Start another capture</button> + <p class="legend">{{ $t("save-capture.capture_download") }}<br /><br /><br /></p> + <button class="btn btn-primary" v-on:click="new_capture()">{{ $t("save-capture.start_capture_btn") }}</button> <iframe :src="download_url" class="frame-download"></iframe> </div> </div> @@ -144,7 +144,8 @@ export default { usb: false, saved: false, save_usb: false, - init: false + init: false, + translation: {} } }, props: { @@ -174,27 +175,18 @@ export default { new_capture: function() { clearTimeout(this.timeout); router.push({ name: 'generate-ap' }) - }, - load_config: function() { - axios.get(`/api/misc/config`, { timeout: 60000 }) - .then(response => { - if(response.data.download_links){ - this.init = true - this.save_usb = false - this.download_url = `/api/save/save-capture/${this.capture_token}/url` - } else { - this.init = true - this.save_usb = true - this.interval = setInterval(() => { this.check_usb() }, 500); - } - }) - .catch(error => { - console.log(error) - }); } }, created: function() { - this.load_config() + if(window.config.download_links){ + this.init = true + this.save_usb = false + this.download_url = `/api/save/save-capture/${this.capture_token}/url` + } else { + this.init = true + this.save_usb = true + this.interval = setInterval(() => { this.check_usb() }, 500); + } } } </script> \ No newline at end of file diff --git a/app/frontend/src/views/splash-screen.vue b/app/frontend/src/views/splash-screen.vue index a915982..bc76224 100644 --- a/app/frontend/src/views/splash-screen.vue +++ b/app/frontend/src/views/splash-screen.vue @@ -15,58 +15,34 @@ data() { return { list_ssids: [], - internet: false, - iface_out:"" + internet: false } }, methods: { - // Check if the device is connected to internet. internet_check: function() { - axios.get(`/api/network/status`, { timeout: 10000 }) + axios.get('/api/network/status', { timeout: 10000 }) .then(response => { - if (response.data.internet){ - this.internet = true - } - this.load_config() + if (response.data.internet) this.internet = true + if (window.config.iface_out.charAt(0) == 'e') { + setTimeout(function () { this.goto_home(); }.bind(this), 1000); + } else { + this.get_wifi_networks(); + } }) .catch(err => (console.log(err))) }, - // Get the WiFi networks around the box. get_wifi_networks: function() { - axios.get(`/api/network/wifi/list`, { timeout: 10000 }) + axios.get('/api/network/wifi/list', { timeout: 10000 }) .then(response => { this.list_ssids = response.data.networks this.goto_home(); }) .catch(err => (console.log(err))) }, - // Forward the view to home, with some props - // such as (SSIDs, internet & interface) goto_home: function() { var list_ssids = this.list_ssids var internet = this.internet - var iface_out = this.iface_out - router.replace({ name: 'home', params: { list_ssids: list_ssids, internet: internet, iface_out : iface_out } }); - }, - // Get the network_out from the config - // to determine the next steps. - load_config: function() { - axios.get(`/api/misc/config`, { timeout: 60000 }) - .then(response => { - if(response.data.iface_out){ - this.iface_out = response.data.iface_out - // If ethernet, just goto the homepage. - // Else, get wifi networks and then go to home. - if(this.iface_out.charAt(0) == "e"){ - setTimeout(function () { this.goto_home(); }.bind(this), 1000); - } else { - this.get_wifi_networks(); - } - } - }) - .catch(error => { - console.log(error) - }); + router.replace({ name: 'home', params: { list_ssids: list_ssids, internet: internet } }); } }, created: function() { diff --git a/app/frontend/src/views/wifi-select.vue b/app/frontend/src/views/wifi-select.vue index 79b5125..f132f27 100644 --- a/app/frontend/src/views/wifi-select.vue +++ b/app/frontend/src/views/wifi-select.vue @@ -2,41 +2,42 @@ <div :class="[ keyboard == false ? 'center' : '' ]"> <div v-if="keyboard == false"> <div v-if="have_internet"> - <p>You seem to be already connected to a network.<br />Do you want to use the current connection?</p> + <p v-html="$t('wifi-select.already_connected_question')"></p> <div class="empty-action"> - <button class="btn" @click="have_internet = false">No, use another</button> <button class="btn" :class="[ connecting ? 'loading' : '', success ? 'btn-success' : 'btn-primary', ]" @click="$router.push({ name: 'generate-ap' })">Yes, use it.</button> + <button class="btn" @click="have_internet = false">{{ $t("wifi-select.no_btn") }}</button>   + <button class="btn" :class="[ connecting ? 'loading' : '', success ? 'btn-success' : 'btn-primary', ]" @click="$router.push({ name: 'generate-ap' })">{{ $t("wifi-select.yes_btn") }}</button> </div> </div> <div v-else> <div v-if="enter_creds" class="wifi-login"> <div class="form-group"> <select class="form-select" id="ssid-select" v-model="ssid"> - <option value="" selected>Wifi name</option> + <option value="" selected>{{ $t("wifi-select.wifi_name") }}</option> <option v-for="ssid in ssids" v-bind:key="ssid.ssid"> {{ ssid.ssid }} </option> </select> </div> <div class="form-group"> - <input class="form-input" type="password" id="password" v-model="password" placeholder="Wifi password" v-on:click="keyboard = (virtual_keyboard)? true : false"> + <input class="form-input" type="password" id="password" v-model="password" :placeholder="$t('wifi-select.wifi_password')" v-on:click="keyboard = (virtual_keyboard)? true : false"> </div> <div class="form-group"> <button class="btn width-100" :class="[ connecting ? 'loading' : '', success ? 'btn-success' : 'btn-primary', ]" v-on:click="wifi_setup()">{{ btnval }}</button> </div> <div class="form-group"> - <button class="btn width-100" :class="[ refreshing ? 'loading' : '' ]" v-on:click="refresh_wifi_list()">Refresh networks list</button> + <button class="btn width-100" :class="[ refreshing ? 'loading' : '' ]" v-on:click="refresh_wifi_list()">{{ $t("wifi-select.refresh_btn") }}</button> </div> </div> <div v-else> - <p><strong>You seem to not be connected to Internet.</strong><br />Please configure the Wi-Fi connection.</p> + <p><strong>{{ $t("wifi-select.not_connected") }}</strong><br />{{ $t("wifi-select.please_config") }}</p> <div class="empty-action"> - <button class="btn btn-primary" @click="enter_creds = true">Ok, let's do that.</button> + <button class="btn btn-primary" @click="enter_creds = true">{{ $t("wifi-select.lets_do_btn") }}</button> </div> </div> </div> </div> <div v-else> - <input :value="input" class="keyboardinput" @input="onInputChange" placeholder="Tap on the virtual keyboard to start"> + <input :value="input" class="keyboardinput" @input="onInputChange" :placeholder="$t('wifi-select.tap_keyboard')"> <SimpleKeyboard @onChange="onChange" @onKeyPress="onKeyPress" :input="input" /> </div> </div> @@ -63,17 +64,18 @@ export default { connecting: false, error: false, success: false, - btnval: "Connect to it.", + btnval: this.$t("wifi-select.connect_to_it"), ssid: "", selected_ssid: false, password: "", keyboard: false, input: "", ssids: [], + virtual_keyboard: window.config.virtual_keyboard, have_internet: false, enter_creds: false, - virtual_keyboard: false, - refreshing: false + refreshing: false, + translation: {} } }, props: { @@ -83,15 +85,15 @@ export default { }, methods: { wifi_connect: function() { - axios.get(`/api/network/wifi/connect`, { timeout: 60000 }) + axios.get('/api/network/wifi/connect', { timeout: 60000 }) .then(response => { if (response.data.status) { this.success = true this.connecting = false - this.btnval = "Wifi connected!" + this.btnval = this.$t('wifi-select.wifi_connected') setTimeout(() => router.push('generate-ap'), 1000); } else { - this.btnval = "Wifi not connected. Please retry." + this.btnval = this.$t('wifi-select.wifi_not_connected') this.connecting = false } }) @@ -101,7 +103,7 @@ export default { }, wifi_setup: function() { if (this.ssid.length && this.password.length >= 8 ){ - axios.post(`/api/network/wifi/setup`, { ssid: this.ssid, password: this.password }, { timeout: 60000 }) + axios.post('/api/network/wifi/setup', { ssid: this.ssid, password: this.password }, { timeout: 60000 }) .then(response => { if(response.data.status) { this.connecting = true @@ -115,15 +117,6 @@ export default { }); } }, - load_config: function() { - axios.get(`/api/misc/config`, { timeout: 60000 }) - .then(response => { - this.virtual_keyboard = response.data.virtual_keyboard - }) - .catch(error => { - console.log(error) - }); - }, onChange(input) { this.input = input this.password = this.input; @@ -140,23 +133,22 @@ export default { }, refresh_wifi_list: function(){ this.refreshing = true - axios.get(`/api/network/wifi/list`, { timeout: 10000 }) - .then(response => { - this.refreshing = false - this.append_ssids(response.data.networks) - }).catch(error => { - this.refreshing = false - console.log(error) + axios.get('/api/network/wifi/list', { timeout: 10000 }) + .then(response => { + this.refreshing = false + this.append_ssids(response.data.networks) + }).catch(error => { + this.refreshing = false + console.log(error) }); } }, created: function() { - this.load_config() this.have_internet = (this.internet) ? true : false this.keyboard = false - if (typeof this.list_ssids == "object" && this.list_ssids.length != 0){ + if (typeof this.list_ssids == 'object' && this.list_ssids.length != 0){ this.ssids = this.list_ssids } else { this.refresh_wifi_list() diff --git a/config.yaml b/config.yaml index 178fec0..633a577 100644 --- a/config.yaml +++ b/config.yaml @@ -48,6 +48,7 @@ frontend: remote_access: true sparklines: true virtual_keyboard: true + user_lang: userlang # NETWORK - # Some elements related to the network configuration, such as diff --git a/install.sh b/install.sh index 474fdb0..eec8071 100644 --- a/install.sh +++ b/install.sh @@ -6,6 +6,7 @@ HOST="$( hostname )" IFACES="$( ifconfig -a | grep -Eo '[a-z0-9]{4,14}\: ' | grep -oE [a-z0-9]+ )" IFACE_OUT="" IFACE_IN="" +LOCALES=(en fr) welcome_screen() { cat << "EOF" @@ -40,6 +41,22 @@ check_operating_system() { fi } +set_userlang() { + # Set the user language. + echo -e "\e[39m[+] Setting the user language...\e[39m" + printf -v joined '%s/' "${LOCALES[@]}" + echo -n " Please choose a language for the reports and the interface (${joined%/}): " + read lang + + if [[ " ${LOCALES[@]} " =~ " ${lang} " ]]; then + sed -i "s/userlang/${lang}/g" /usr/share/tinycheck/config.yaml + echo -e "\e[92m [✔] User language setted!\e[39m" + else + echo -e "\e[91m [✘] You must choose between the languages proposed, let's retry.\e[39m" + set_userlang + fi +} + set_credentials() { # Set the credentials to access to the backend. echo -e "\e[39m[+] Setting the backend credentials...\e[39m" @@ -410,6 +427,7 @@ else check_operating_system check_interfaces create_directory + set_userlang set_credentials check_dependencies configure_dnsmask diff --git a/server/frontend/app/blueprints/misc.py b/server/frontend/app/blueprints/misc.py index 6964121..c9bbeed 100644 --- a/server/frontend/app/blueprints/misc.py +++ b/server/frontend/app/blueprints/misc.py @@ -4,14 +4,17 @@ import subprocess as sp from flask import Blueprint, jsonify from app.utils import read_config +import re +import sys +import os misc_bp = Blueprint("misc", __name__) @misc_bp.route("/reboot", methods=["GET"]) def api_reboot(): - """ - Reboot the device + """ + Reboot the device """ if read_config(("frontend", "reboot_option")): sp.Popen("shutdown -r now", shell=True) @@ -22,8 +25,8 @@ def api_reboot(): @misc_bp.route("/quit", methods=["GET"]) def api_quit(): - """ - Quit the interface (Chromium browser) + """ + Quit the interface (Chromium browser) """ if read_config(("frontend", "quit_option")): sp.Popen('pkill -INT -f "chromium-browser"', shell=True) @@ -34,8 +37,8 @@ def api_quit(): @misc_bp.route("/shutdown", methods=["GET"]) def api_shutdown(): - """ - Reboot the device + """ + Reboot the device """ if read_config(("frontend", "shutdown_option")): sp.Popen("shutdown -h now", shell=True) @@ -46,8 +49,8 @@ def api_shutdown(): @misc_bp.route("/config", methods=["GET"]) def get_config(): - """ - Get configuration keys relative to the GUI + """ + Get configuration keys relative to the GUI """ return jsonify({ "virtual_keyboard": read_config(("frontend", "virtual_keyboard")), @@ -57,5 +60,6 @@ def get_config(): "quit_option": read_config(("frontend", "quit_option")), "shutdown_option": read_config(("frontend", "shutdown_option")), "reboot_option": read_config(("frontend", "reboot_option")), - "iface_out": read_config(("network", "out")) + "iface_out": read_config(("network", "out")), + "user_lang": read_config(("frontend", "user_lang")) })
{}{}{}{}