Feat: link checking for phishing detection

Signed-off-by: Hamza Mahjoubi <hamzamahjoubi221@gmail.com>
This commit is contained in:
Hamza Mahjoubi 2024-08-13 16:17:19 +02:00
Родитель 6b86b4d4e2
Коммит c791095a3d
7 изменённых файлов: 155 добавлений и 5 удалений

Просмотреть файл

@ -27,6 +27,7 @@
"bytestream/horde-util": "^2.7.0",
"cerdic/css-tidy": "v2.1.0",
"ezyang/htmlpurifier": "4.17.0",
"glenscott/url-normalizer": "^1.4",
"gravatarphp/gravatar": "dev-master#6b9f6a45477ce48285738d9d0c3f0dbf97abe263",
"hamza221/html2text": "^1.0",
"jeremykendall/php-domain-parser": "^6.3",

43
composer.lock сгенерированный
Просмотреть файл

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "198b503625a39a6c6a9108a94acdea9e",
"content-hash": "0cc234da73403beae6482cd827b5dc06",
"packages": [
{
"name": "amphp/amp",
@ -1706,6 +1706,47 @@
},
"time": "2023-11-17T15:01:25+00:00"
},
{
"name": "glenscott/url-normalizer",
"version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/glenscott/url-normalizer.git",
"reference": "b8e79d3360a1bd7182398c9956bd74d219ad1b3c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/glenscott/url-normalizer/zipball/b8e79d3360a1bd7182398c9956bd74d219ad1b3c",
"reference": "b8e79d3360a1bd7182398c9956bd74d219ad1b3c",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": ">=5.3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"URL\\": "src/URL"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Glen Scott",
"email": "glen@glenscott.co.uk"
}
],
"description": "Syntax based normalization of URL's",
"support": {
"issues": "https://github.com/glenscott/url-normalizer/issues",
"source": "https://github.com/glenscott/url-normalizer/tree/master"
},
"time": "2015-06-11T16:06:02+00:00"
},
{
"name": "gravatarphp/gravatar",
"version": "dev-master",

Просмотреть файл

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail\Service\PhishingDetection;
use OCA\Mail\PhishingDetectionResult;
use OCP\IL10N;
use URL\Normalizer;
class LinkCheck {
protected IL10N $l10n;
public function __construct(IL10N $l10n) {
$this->l10n = $l10n;
}
// checks if link text is meant to look like a link
private function textLooksLikeALink(string $text): bool {
// based on https://gist.github.com/gruber/8891611
$pattern = '/(?i)\b((?:https?:(?:\/{1,3}|[a-z0-9%])|[a-z0-9.\-]+[.](?:com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|xxx|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|Ja|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)\/)(?:[^\s()<>{}\[\]]+|\([^\s()]*?\([^\s()]+\)[^\s()]*?\)|\([^\s]+?\))+(?:\([^\s()]*?\([^\s()]+\)[^\s()]*?\)|\([^\s]+?\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’])|(?:(?<!@)[a-z0-9]+(?:[.\-][a-z0-9]+)*[.](?:com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|xxx|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|Ja|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)\b\/?(?!@)))/';
return preg_match($pattern, $text) === 1;
}
private function getInnerText(\DOMElement $node) : string {
$innerText = '';
foreach ($node->childNodes as $child) {
if ($child->nodeType === XML_TEXT_NODE) {
$innerText .= $child->nodeValue;
} elseif ($child->nodeType === XML_ELEMENT_NODE) {
$innerText .= $this->getInnerText($child);
}
}
return $innerText;
}
public function run(string $htmlMessage) : PhishingDetectionResult {
$results = [];
$zippedArray = [];
$dom = new \DOMDocument();
libxml_use_internal_errors(true);
$dom->loadHTML($htmlMessage);
libxml_use_internal_errors();
$anchors = $dom->getElementsByTagName('a');
foreach ($anchors as $anchor) {
$href = $anchor->getAttribute('href');
$linkText = $this->getInnerText($anchor);
$zippedArray[] = [
'href' => $href,
'linkText' => $linkText
];
}
foreach ($zippedArray as $zipped) {
$un = new Normalizer($zipped['href']);
$url = $un->normalize();
if($this->textLooksLikeALink($zipped['linkText'])) {
if(parse_url($url, PHP_URL_HOST) !== parse_url($zipped['linkText'], PHP_URL_HOST)) {
$results[] = [
'href' => $url,
'linkText' => $zipped['linkText'],
];
}
}
}
if(count($results) > 0) {
return new PhishingDetectionResult(PhishingDetectionResult::LINK_CHECK, true, $this->l10n->t('Some addresses in this message are not matching the link text'), $results);
}
return new PhishingDetectionResult(PhishingDetectionResult::LINK_CHECK, false);
}
}

Просмотреть файл

@ -14,11 +14,12 @@ use OCA\Mail\AddressList;
use OCA\Mail\PhishingDetectionList;
class PhishingDetectionService {
public function __construct(private ContactCheck $contactCheck, private CustomEmailCheck $customEmailCheck, private DateCheck $dateCheck, private ReplyToCheck $replyToCheck) {
public function __construct(private ContactCheck $contactCheck, private CustomEmailCheck $customEmailCheck, private DateCheck $dateCheck, private ReplyToCheck $replyToCheck, private LinkCheck $linkCheck) {
$this->contactCheck = $contactCheck;
$this->customEmailCheck = $customEmailCheck;
$this->dateCheck = $dateCheck;
$this->replyToCheck = $replyToCheck;
$this->linkCheck = $linkCheck;
}
@ -38,6 +39,9 @@ class PhishingDetectionService {
$list->addCheck($this->contactCheck->run($fromFN, $fromEmail));
$list->addCheck($this->dateCheck->run($date));
$list->addCheck($this->customEmailCheck->run($fromEmail, $customEmail));
if ($hasHtmlMessage) {
$list->addCheck($this->linkCheck->run($htmlMessage));
}
return $list->jsonSerialize();
}
}

Просмотреть файл

@ -11,16 +11,28 @@
<ul v-for="(warning,index) in warnings" :key="index" class="warning__list">
<li>{{ warning.message }}</li>
</ul>
<div v-if="linkWarning !== undefined" class="warning__links">
<NcButton class="warning__links__button" type="Tertiary" @click="showMore = !showMore">
{{ showMore? t('mail','hide suspicious links') :t('mail','Show suspicious links') }}
</NcButton>
<div v-if="showMore">
<ul v-for="(link,index) in linkWarning.additionalData" :key="index" class="warning__list">
<li><b>href: </b>{{ link.href }} : <b>{{ t('mail','link text') }}</b> {{ link.linkText }} </li>
</ul>
</div>
</div>
</div>
</template>
<script>
import IconAlertOutline from 'vue-material-design-icons/AlertOutline.vue'
import { NcButton } from '@nextcloud/vue'
export default {
name: 'PhishingWarning',
components: {
IconAlertOutline,
NcButton,
},
props: {
phishingData: {
@ -37,6 +49,9 @@ export default {
warnings() {
return this.phishingData.filter(check => check.isPhishing)
},
linkWarning() {
return this.phishingData.find(check => check.type === 'Link')
},
},
}

Просмотреть файл

@ -14,6 +14,7 @@ use OCA\Mail\Service\ContactsIntegration;
use OCA\Mail\Service\PhishingDetection\ContactCheck;
use OCA\Mail\Service\PhishingDetection\CustomEmailCheck;
use OCA\Mail\Service\PhishingDetection\DateCheck;
use OCA\Mail\Service\PhishingDetection\LinkCheck;
use OCA\Mail\Service\PhishingDetection\PhishingDetectionService;
use OCA\Mail\Service\PhishingDetection\ReplyToCheck;
use OCP\AppFramework\Utility\ITimeFactory;
@ -30,6 +31,7 @@ class PhishingDetectionServiceIntegrationTest extends TestCase {
private CustomEmailCheck $customEmailCheck;
private DateCheck $dateCheck;
private ReplyToCheck $replyToCheck;
private LinkCheck $linkCheck;
private PhishingDetectionService $service;
protected function setUp(): void {
@ -40,7 +42,8 @@ class PhishingDetectionServiceIntegrationTest extends TestCase {
$this->customEmailCheck = new CustomEmailCheck($this->l10n);
$this->dateCheck = new DateCheck($this->l10n, \OC::$server->get(ITimeFactory::class));
$this->replyToCheck = new ReplyToCheck($this->l10n);
$this->service = new PhishingDetectionService($this->contactCheck, $this->customEmailCheck, $this->dateCheck, $this->replyToCheck);
$this->linkCheck = new LinkCheck($this->l10n);
$this->service = new PhishingDetectionService($this->contactCheck, $this->customEmailCheck, $this->dateCheck, $this->replyToCheck, $this->linkCheck);
}

Просмотреть файл

@ -13,6 +13,7 @@ use OCA\Mail\PhishingDetectionResult;
use OCA\Mail\Service\PhishingDetection\ContactCheck;
use OCA\Mail\Service\PhishingDetection\CustomEmailCheck;
use OCA\Mail\Service\PhishingDetection\DateCheck;
use OCA\Mail\Service\PhishingDetection\LinkCheck;
use OCA\Mail\Service\PhishingDetection\PhishingDetectionService;
use OCA\Mail\Service\PhishingDetection\ReplyToCheck;
@ -24,6 +25,7 @@ class PhishingDetectionServiceTest extends TestCase {
private CustomEmailCheck|MockObject $customEmailCheck;
private DateCheck|MockObject $dateCheck;
private ReplyToCheck|MockObject $replyToCheck;
private LinkCheck|MockObject $linkCheck;
private PhishingDetectionService $service;
protected function setUp(): void {
@ -32,7 +34,8 @@ class PhishingDetectionServiceTest extends TestCase {
$this->customEmailCheck = $this->createMock(customEmailCheck::class);
$this->dateCheck = $this->createMock(DateCheck::class);
$this->replyToCheck = $this->createMock(ReplyToCheck::class);
$this->service = new PhishingDetectionService($this->contactCheck, $this->customEmailCheck, $this->dateCheck, $this->replyToCheck);
$this->linkCheck = $this->createMock(LinkCheck::class);
$this->service = new PhishingDetectionService($this->contactCheck, $this->customEmailCheck, $this->dateCheck, $this->replyToCheck, $this->linkCheck);
}
@ -56,7 +59,10 @@ class PhishingDetectionServiceTest extends TestCase {
$this->customEmailCheck->expects($this->once())
->method('run')
->willReturn(new PhishingDetectionResult(PhishingDetectionResult::CUSTOM_EMAIL_CHECK, false));
$result = $this->service->checkHeadersForPhishing($parsedHeaders, false);
$this->linkCheck->expects($this->once())
->method('run')
->willReturn(new PhishingDetectionResult(PhishingDetectionResult::LINK_CHECK, false));
$result = $this->service->checkHeadersForPhishing($parsedHeaders, true, '');
$this->assertFalse($result["warning"]);
}