diff --git a/.arcunit b/.arcunit new file mode 100644 index 0000000..860ee1a --- /dev/null +++ b/.arcunit @@ -0,0 +1,8 @@ +{ + "engines": { + "phutil": { + "type": "phutil", + "include": "(\\.php$)" + } + } +} diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 807660b..5c4a33e 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,18 +1,20 @@ 2, 'class' => array( 'SecureShieldsUpAction' => 'abuse/SecureSheldsUpAction.php', + 'SecureShieldsUpTestCase' => 'abuse/__tests__/SecureSheldsUpTestCase.php', ), 'function' => array(), 'xmap' => array( 'SecureShieldsUpAction' => 'HeraldAction', + 'SecureShieldsUpTestCase' => 'PhabricatorTestCase', ), )); diff --git a/src/abuse/SecureSheldsUpAction.php b/src/abuse/SecureSheldsUpAction.php index 8a44bec..5509787 100644 --- a/src/abuse/SecureSheldsUpAction.php +++ b/src/abuse/SecureSheldsUpAction.php @@ -1,160 +1,187 @@ establishConnection('r'), 'SELECT authorPHID FROM %T WHERE objectPHID = %s ORDER BY id DESC LIMIT 1', id(new ManiphestTransaction())->getTableName(), $object->getPHID()); if (!$last_actor_row) { return; } $actor = id(new PhabricatorPeopleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($last_actor_row['authorPHID'])) ->executeOne(); if (!$actor) { return; } if ($this->isFriendlyUser($actor)) { return; } if (!$this->isHostileObject($object)) { return; } $this->quarantineUser($actor); $this->quarantineObject($object); $this->logEffect(self::DO_SHIELD); } public function getHeraldActionStandardType() { return self::STANDARD_NONE; } public function renderActionDescription($value) { return pht('Shields up.'); } protected function getActionEffectMap() { return array( self::DO_SHIELD => array( 'icon' => 'fa-umbrella', 'color' => 'indigo', 'name' => pht('Shields Up'), ), ); } protected function renderActionEffectDescription($type, $data) { switch ($type) { case self::DO_SHIELD: return pht('Shields up.'); } } private function isFriendlyUser(PhabricatorUser $user) { if ($user->getIsAdmin()) { return true; } return false; } private function isHostileObject($object) { $content = array(); if ($object instanceof ManiphestTask) { $content[] = $object->getTitle(); $content[] = $object->getDescription(); } $content = implode("\n\n", $content); $patterns = array(); // Phone numbers that we'll reject. $numbers = array( '8443133901', '8007909186', '800059007', '8008101018', + '8002044122', + '8007992667', + '8557092847', ); + if (self::matchPhoneNumbers($numbers, $content)) { + return true; + } + + return false; + } + + public static function matchPhoneNumbers(array $numbers, $content) { + $swap = array( + 'o' => '0', + 'O' => '0', + '@' => '0', + '()' => '0', + + 'i' => '1', + 'I' => '1', + '|' => '1', + 'l' => '1', + ); + + $content = str_replace( + array_keys($swap), + array_values($swap), + $content); + foreach ($numbers as $number) { $regex = array(); for ($ii = 0; $ii < strlen($number); $ii++) { $regex[] = $number[$ii]; } // Reject all variants of the number with other random punctuation or - // spaces betwee the digits. + // spaces between the digits. $regex = implode('[^\\d]{0,6}', $regex); $patterns[] = '/'.$regex.'/'; } foreach ($patterns as $pattern) { if (preg_match($pattern, $content)) { return true; } } return false; } - private function quarantineUser(PhabricatorUser $user) { // For now, just log the user out of all their sessions so it's not a big // deal if we hit a friendly user by accident. We could make this more // extreme in the future. $sessions = id(new PhabricatorAuthSessionQuery()) ->setViewer($user) ->withIdentityPHIDs(array($user->getPHID())) ->execute(); foreach ($sessions as $session) { $session->delete(); } } private function quarantineObject($object) { $title = $object->getTitle(); $new_title = ' '.$title; $object ->setTitle($new_title) ->setViewPolicy(PhabricatorPolicies::POLICY_ADMIN) ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) ->save(); } } diff --git a/src/abuse/__tests__/SecureSheldsUpTestCase.php b/src/abuse/__tests__/SecureSheldsUpTestCase.php new file mode 100644 index 0000000..cb563e1 --- /dev/null +++ b/src/abuse/__tests__/SecureSheldsUpTestCase.php @@ -0,0 +1,38 @@ + true, + '1 (800) 204.4122' => true, + '80012044122' => false, + + '8OO2o44I22' => true, + + // Does not contain the number. + 'Pulse Rifle' => false, + + // Currently, we give up after 6 characters without finding the next + // digit. + '800........204.4122' => false, + + // We aren't wizards, but users aren't either. + 'eight hundred, then dial two zero 4, then 41 and finally twenty two' + => false, + ); + + foreach ($tests as $input => $expect) { + $actual = SecureShieldsUpAction::matchPhoneNumbers($numbers, $input); + $this->assertEqual( + $expect, + $actual, + pht('Detection of phone numbers in: %s', $input)); + } + } + +}