From 1a75bc48b18318af0fb0c5fcebb4cc3f0bad08d9 Mon Sep 17 00:00:00 2001 From: "W. Leighton Dawson" Date: Sat, 1 Apr 2023 00:50:18 +0200 Subject: [PATCH] T5281 - Port CC Token (#205) --- .github/workflows/test.yml | 2 +- canarytokens/canarydrop.py | 36 +- canarytokens/channel.py | 2 - canarytokens/channel_http.py | 19 +- canarytokens/channel_input_smtp.py | 1 - canarytokens/channel_output_email.py | 1 - canarytokens/channel_output_webhook.py | 2 - canarytokens/datagen.py | 14 + canarytokens/extendtoken.py | 381 ++++++++++++++++++ canarytokens/models.py | 158 +++++++- canarytokens/msword.py | 1 - canarytokens/pdfgen.py | 1 - canarytokens/queries.py | 9 +- canarytokens/settings.py | 5 +- canarytokens/tokens.py | 19 +- frontend/app.py | 82 +++- poetry.lock | 23 +- pyproject.toml | 1 + templates/emails/notification.html | 18 + templates/generate_new.html | 212 +++++++--- templates/manage_new.html | 44 ++ templates/robots.txt | 7 +- templates/security.txt | 12 + templates/static/azure-id.png | Bin 0 -> 85496 bytes templates/static/cc-background-AMEX.png | Bin 0 -> 107871 bytes templates/static/copy-icon.svg | 1 + templates/static/credit-card.png | Bin 0 -> 54429 bytes templates/static/download.svg | 3 + templates/static/msreg.png | Bin 0 -> 7324 bytes templates/static/styles.css | 77 +++- templates/static/styles.min.css | 2 +- .../integration/test_against_token_server.py | 38 -- tests/units/test_extendtoken.py | 24 ++ tests/units/test_frontend.py | 5 + tests/units/test_http_channel.py | 105 ++++- 35 files changed, 1152 insertions(+), 153 deletions(-) create mode 100644 canarytokens/datagen.py create mode 100644 canarytokens/extendtoken.py create mode 100644 templates/security.txt create mode 100644 templates/static/azure-id.png create mode 100755 templates/static/cc-background-AMEX.png create mode 100644 templates/static/copy-icon.svg create mode 100644 templates/static/credit-card.png create mode 100644 templates/static/download.svg create mode 100644 templates/static/msreg.png create mode 100644 tests/units/test_extendtoken.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7fb6cbf1f..76190a11b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: push: branches: - "T4627_py3_main" - - "T5282_add_mailgun" + - "T5281_port_cc_token" jobs: tests: diff --git a/canarytokens/canarydrop.py b/canarytokens/canarydrop.py index 87cdcd7ae..117e232cf 100644 --- a/canarytokens/canarydrop.py +++ b/canarytokens/canarydrop.py @@ -16,7 +16,7 @@ from datetime import datetime, timedelta from hashlib import md5 from pathlib import Path -from typing import Any, Dict, List, Literal, Optional, Union +from typing import Any, Literal, Optional, Union from pydantic import BaseModel, EmailStr, Field, HttpUrl, parse_obj_as, root_validator @@ -118,6 +118,17 @@ class Canarydrop(BaseModel): wg_key: Optional[str] # cmd specific stuff cmd_process: Optional[str] + # CC specific stuff + cc_id: Optional[str] + cc_kind: Optional[str] + cc_number: Optional[str] + cc_cvc: Optional[str] + cc_expiration: Optional[str] + cc_name: Optional[str] + cc_billing_zip: Optional[str] + cc_address: Optional[str] + cc_rendered_html: Optional[str] + cc_rendered_csv: Optional[str] @root_validator(pre=True) def _validate_triggered_details(cls, values): @@ -166,7 +177,7 @@ class Config: def add_additional_info_to_hit( self, hit_time: str, - additional_info: Dict[str, str], + additional_info: dict[str, str], ) -> None: """ """ trigger_details = queries.get_canarydrop_triggered_details(self.canarytoken) @@ -232,11 +243,15 @@ def get_url_components( queries.get_all_canary_pages(), ) - def generate_random_url(self, canary_domains: List[str]): + def generate_random_url(self, canary_domains: list[str]): """ Return a URL generated at random with the saved Canarytoken. The random URL is also saved into the Canarydrop. """ + # TODO: check how we want this caching to work. Use a property if needed. + # Or @lru.cache() or as it was but it's non-obvious + if self.generated_url: + return self.generated_url (path_elements, pages) = self.get_url_components() generated_url = random.choice(canary_domains) + "/" @@ -252,13 +267,11 @@ def generate_random_url(self, canary_domains: List[str]): path.append(pages[random.randint(0, len(pages) - 1)]) generated_url += "/".join(path) - # TODO: check how we want this caching to work. Use a property if needed. - # Or @lru.cache() or as it was but it's non-obvious - # self.generated_url = generated_url - # self.generated_url + # cache + self.generated_url = generated_url return generated_url - def get_url(self, canary_domains: List[str]): + def get_url(self, canary_domains: list[str]): return self.generate_random_url(canary_domains) def generate_random_hostname(self, with_random=False, nxdomain=False): @@ -281,7 +294,6 @@ def generate_random_hostname(self, with_random=False, nxdomain=False): return generated_hostname def get_hostname(self, with_random=False, as_url=False, nxdomain=False): - random_hostname = self.generate_random_hostname( with_random=with_random, nxdomain=nxdomain, @@ -344,7 +356,7 @@ def get_requested_output_channels( ): """Return a list containing the output channels configured in this Canarydrop.""" - channels: List[str] = [] + channels: list[str] = [] if self.alert_email_enabled and self.alert_email_recipient: channels.append(OUTPUT_CHANNEL_EMAIL) if self.alert_webhook_enabled and self.alert_webhook_url: @@ -449,12 +461,12 @@ def get_csv_incident_list(self) -> str: return csvOutput.getvalue() - def format_triggered_details_of_history_page(self) -> Dict[str, Any]: + def format_triggered_details_of_history_page(self) -> dict[str, Any]: """ Helper function as history.html still relies on v2 format. TODO: remove this when history.html is updated. Returns: - Dict[str, Any]: v2 formatted incident list. + dict[str, Any]: v2 formatted incident list. """ return self.triggered_details.serialize_for_v2(readable_time_format=True) diff --git a/canarytokens/channel.py b/canarytokens/channel.py index 24d209c63..dc2ee9ea1 100644 --- a/canarytokens/channel.py +++ b/canarytokens/channel.py @@ -173,7 +173,6 @@ def format_email_canaryalert( protocol: str, host: str, # DESIGN: Shift this to settings. Do we need to have this logic here? ) -> TokenAlertDetails: - details = cls.gather_alert_details( canarydrop, protocol=protocol, @@ -329,7 +328,6 @@ def send_alert( canarydrop: Canarydrop, token_hit: AnyTokenHit, ) -> None: - self.do_send_alert( input_channel=input_channel, canarydrop=canarydrop, diff --git a/canarytokens/channel_http.py b/canarytokens/channel_http.py index 30beb3ea5..3c0700fa5 100644 --- a/canarytokens/channel_http.py +++ b/canarytokens/channel_http.py @@ -8,7 +8,7 @@ # from canarytokens.channel_dns import create_token_hit from twisted.web import resource, server from twisted.web.resource import EncodingResourceWrapper, Resource -from twisted.web.server import GzipEncoderFactory +from twisted.web.server import GzipEncoderFactory, Request from canarytokens import queries from canarytokens.channel import InputChannel @@ -43,7 +43,7 @@ def getChild(self, name, request): return self return Resource.getChild(self, name, request) - def render_GET(self, request): + def render_GET(self, request: Request): # A GET request to a token URL can trigger one of a few responses: # 1. Check if link has been clicked on (rather than loaded from an # ) by looking at the Accept header, then: @@ -54,7 +54,18 @@ def render_GET(self, request): # 2b. Serve our default 1x1 gif try: - canarytoken = Canarytoken(value=request.path) + manage_uris = [ + b"/generate", + b"/download?", + b"/history?", + b"/manage?", + b"/resources/", + b"/settings", + ] + if any([request.path.find(x) >= 0 for x in manage_uris]): + canarytoken = Canarytoken(value=request.path) + else: + canarytoken = Canarytoken(value=request.uri) except NoCanarytokenFound as e: log.info( f"HTTP GET on path {request.path} did not correspond to a token. Error: {e}" @@ -98,7 +109,7 @@ def render_GET(self, request): request.setHeader("Server", "Apache") return resp - def render_POST(self, request): + def render_POST(self, request: Request): try: token = Canarytoken(value=request.path) except NoCanarytokenFound as e: diff --git a/canarytokens/channel_input_smtp.py b/canarytokens/channel_input_smtp.py index f3fdfad20..452b4f080 100644 --- a/canarytokens/channel_input_smtp.py +++ b/canarytokens/channel_input_smtp.py @@ -137,7 +137,6 @@ def __init__(self, **kwargs): def greeting( self, ): - self.src_ip = self.transport.getPeer().host try: return self.factory.responses["greeting"] diff --git a/canarytokens/channel_output_email.py b/canarytokens/channel_output_email.py index 4ddfe9400..d9422fa76 100644 --- a/canarytokens/channel_output_email.py +++ b/canarytokens/channel_output_email.py @@ -225,7 +225,6 @@ def sendgrid_send( email_subject: str, sandbox_mode: bool = False, ) -> tuple[bool, str]: - sendgrid_client = sendgrid.SendGridAPIClient( api_key=api_key.get_secret_value().strip() ) diff --git a/canarytokens/channel_output_webhook.py b/canarytokens/channel_output_webhook.py index 38f066cca..872290a52 100644 --- a/canarytokens/channel_output_webhook.py +++ b/canarytokens/channel_output_webhook.py @@ -25,7 +25,6 @@ def do_send_alert( canarydrop: canarydrop.Canarydrop, token_hit: AnyTokenHit, ) -> None: - payload = input_channel.format_webhook_canaryalert( canarydrop=canarydrop, host=self.frontend_hostname, @@ -42,7 +41,6 @@ def generic_webhook_send( payload: Dict[str, str], alert_webhook_url: HttpUrl, ) -> None: - # Design: wrap in a retry? try: response = requests.post( diff --git a/canarytokens/datagen.py b/canarytokens/datagen.py new file mode 100644 index 000000000..f74e29802 --- /dev/null +++ b/canarytokens/datagen.py @@ -0,0 +1,14 @@ +from faker import Faker + +fake = Faker() + + +def generate_person() -> dict[str, str]: + address = fake.address() + billing_zip = address.split(" ")[-1] + return { + "first_name": fake.first_name(), + "last_name": fake.last_name(), + "address": address, + "billing_zip": billing_zip, + } diff --git a/canarytokens/extendtoken.py b/canarytokens/extendtoken.py new file mode 100644 index 000000000..c3d7f5716 --- /dev/null +++ b/canarytokens/extendtoken.py @@ -0,0 +1,381 @@ +import datetime +import json +import os +from typing import Optional + +import requests + +from canarytokens.datagen import generate_person +from canarytokens.models import ApiProvider, CreditCard + + +class ExtendAPIException(Exception): + pass + + +class ExtendAPIRateLimitException(Exception): + pass + + +class ExtendAPICardsException(Exception): + pass + + +class ExtendAPI(ApiProvider): + """Class for interacting with the Extend API for virtual card management""" + + def __init__( + self, + email, + password, + card_name, + token=None, + ): + self.email = email + self.token = token + self.kind = "AMEX" + self.card_name = card_name + if self.token: + return + + req = self._post_api( + "https://api.paywithextend.com/signin", + {"email": self.email, "password": password}, + ) + self.token = req.json().get("token") + self.refresh_token = req.json().get("refresh_token") + + def _post_api(self, endpoint: str, data: Optional[str] = None) -> requests.Response: + """Performs a POST against the passed endpoint with the data passed""" + headers = { + "Content-Type": "application/json", + "Accept": "application/vnd.paywithextend.v2021-03-12+json", + } + if self.token is not None: + headers["Authorization"] = "Bearer {}".format(self.token) + resp = requests.post(endpoint, json=data, headers=headers) + if resp.status_code == 422: + raise ExtendAPIRateLimitException( + "ExtendAPI call failed with 422 rate limit." + ) + print(resp.content) + try: + json_error, text_error = resp.json().get("error", ""), "" + except requests.exceptions.JSONDecodeError: + json_error, text_error = "", resp.text + if resp.status_code != 200 or json_error: + raise ExtendAPIException( + "ExtendAPI call failed. Response code {}, error={}".format( + resp.status_code, repr(json_error + text_error) + ) + ) + return resp + + def _get_api( + self, endpoint: str, data: Optional[str] = None + ) -> requests.Response: # pragma: no cover + """Performs a GET against the passed endpoint""" + headers = { + "Content-Type": "application/json", + "Accept": "application/vnd.paywithextend.v2021-03-12+json", + } + if self.token is not None: + headers["Authorization"] = "Bearer {}".format(self.token) + resp = requests.get(endpoint, headers=headers, json=data) + if resp.status_code != 200 or resp.json().get("error", "") != "": + raise ExtendAPIException( + "ExtendAPI call failed. Response code {}, error={}".format( + resp.status_code, resp.json().get("error") + ) + ) + return resp + + def _put_api( + self, endpoint: str, data: Optional[str] = None + ) -> requests.Response: # pragma: no cover + """Performs a PUT against the passed endpoint""" + headers = { + "Content-Type": "application/json", + "Accept": "application/vnd.paywithextend.v2021-03-12+json", + } + if self.token is not None: + headers["Authorization"] = f"Bearer {self.token}" + resp = requests.put(endpoint, headers=headers, json=data) + if resp.status_code != 200 or resp.json().get("error", "") != "": + raise ExtendAPIException( + "ExtendAPI call failed. Response code {}, error={}".format( + resp.status_code, resp.json().get("error") + ) + ) + return resp + + def _delete_api(self, endpoint: str): # pragma: no cover + """Performs a DELETE against the passed endpoint""" + headers = { + "Content-Type": "application/json", + "Accept": "application/vnd.paywithextend.v2021-03-12+json", + } + if self.token: + headers["Authorization"] = f"Bearer {self.token}" + resp = requests.delete(endpoint, headers=headers) + if resp.status_code != 200 or resp.json().get("error", "") != "": + raise ExtendAPIException( + "ExtendAPI call failed. Response code {}, error={}".format( + resp.status_code, resp.json().get("error") + ) + ) + return resp + + def _refresh_auth_token(self): # pragma: no cover + """Refreshes the auth session token""" + req = self._post_api( + "https://api.paywithextend.com/renewauth", + {"refreshToken": self.refresh_token}, + ) + self.token = req.json().get("token") + self.refresh_token = req.json().get("refresh_token") + + @classmethod + def fetch_credentials(cls, path=None): # pragma: no cover + if not path: + raise Exception("No path supplied") + + if not os.path.exists(path): + raise Exception(f"File does not exist: {path}") + + with open(path) as f: + credentials = json.loads(f.read().strip()) + + return credentials["EXTEND_EMAIL_ADDRESS"], credentials["EXTEND_API_KEY"] + + def get_virtual_cards(self) -> list[tuple[str, str]]: # pragma: no cover + """Returns a list of tuples of (card owner, card id)""" + req = self._get_api( + "https://api.paywithextend.com/virtualcards?count=50&page=0" + ) + cards = [] + for vc in req.json().get("virtualCards", []): + cards.append( + ( + vc["recipient"]["firstName"] + " " + vc["recipient"]["lastName"], + vc.get("id"), + ) + ) + return cards + + def get_card_info(self, card_id) -> Optional[dict[str, str]]: + """Returns all the data about a passed card_id available""" + req = self._get_api("https://v.paywithextend.com/virtualcards/" + card_id) + return req.json() + + def get_transaction( + self, txn_id: str + ) -> Optional[dict[str, str]]: # pragma: no cover + """Returns more details about a specific transaction""" + req = self._get_api("https://api.paywithextend.com/transactions/" + txn_id) + return req.json() + + def get_card_transactions(self, card_id) -> list[dict]: # pragma: no cover + """Gets all the recent card transactions for a given card_id""" + req = self._get_api( + "https://api.paywithextend.com/virtualcards/{0}/transactions?status=DECLINED,PENDING,CLEARED".format( + card_id + ) + ) + return req.json().get("transactions", []) + + def get_latest_transaction( + self, cc: CreditCard + ) -> Optional[dict[str, str]]: # pragma: no cover + """Gets the latest transaction for a given credit card""" + txns = self.get_card_transactions(cc.id) + if len(txns) == 0: + return None + max_tx = txns[0] + max_dt = datetime.datetime.fromisoformat(max_tx["authedAt"].split("+")[0]) + for txn in txns: + dt = datetime.datetime.fromisoformat(txn["authedAt"].split("+")[0]) + if dt > max_dt: + max_dt = dt + max_tx = txn + return {max_dt.toisoformat(): max_tx} + + def get_parent_card_id(self) -> str: # pragma: no cover + """Gets the ID of the organization's real CC""" + resp = self._get_api("https://api.paywithextend.com/creditcards") + cards = resp.json().get("creditCards") + # import rpdb; rpdb.set_trace() + if len(cards) == 0: + raise ExtendAPICardsException("No cards returned from Extend") + + filtered_cards = [x for x in cards if x["displayName"] == self.card_name] + + if len(filtered_cards) == 0: + raise ExtendAPICardsException("No card is called {}".format(self.card_name)) + + if len(filtered_cards) > 1: + raise ExtendAPICardsException( + "Multiple cards are called {}".format(self.card_name) + ) + + return filtered_cards[0]["id"] + + def make_card( + self, + token_url: str, + first_name: str, + last_name: str, + address: str, + billing_zip: str, + limit_cents: int = 100, + ) -> CreditCard: + """Creates a new CreditCard via Extend's CreateVirtualCard API""" + cc = self.get_parent_card_id() + now_ts = datetime.datetime.now() - datetime.timedelta(days=1) + now_ts = now_ts.isoformat() + "+0000" + expiry = datetime.datetime.now() + datetime.timedelta(weeks=2 * 52) + future_ts = expiry.isoformat() + "+0000" + expiry_str = str(expiry.month) + "/" + str(expiry.year) + notes = token_url + data = { + "creditCardId": cc, + "recipient": self.email, + "displayName": first_name + " " + last_name + "'s card", + "balanceCents": limit_cents, + "direct": "false", + "recurs": "false", + "validFrom": now_ts, + "validTo": future_ts, + "notes": notes, + "referenceFields": [], + "validMccRanges": [{"lowest": "9403", "highest": "9403"}], + } + out = CreditCard( + id="", + name=first_name + " " + last_name, + number=None, + cvc=None, + billing_zip=billing_zip, + expiration=expiry_str, + address=address, + kind=self.kind, + ) + req = self._post_api("https://api.paywithextend.com/virtualcards", data=data) + out.id = req.json().get("virtualCard")["id"] + vc_info = self.get_card_info(out.id) + if "vcn" in vc_info["virtualCard"].keys(): + out.cvc = vc_info["virtualCard"]["securityCode"] + out.number = vc_info["virtualCard"]["vcn"] + else: + print("ERROR GETTING CARD DETAILS") + return out + + def cancel_card(self, card_id) -> None: # pragma: no cover + """Cancels a passed card""" + _ = self._put_api( + "https://api.paywithextend.com/virtualcards/" + card_id + "/cancel" + ) + + def create_credit_card( + self, + token_url: str, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + address: Optional[str] = None, + billing_zip: Optional[str] = None, + ) -> CreditCard: + """Creates a cardholder and associated virtual card for the passed person, if not passed, will generate fake data to use""" + fake_person = generate_person() + if first_name is None: + first_name = fake_person["first_name"] + if last_name is None: + last_name = fake_person["last_name"] + if address is None: + address = fake_person["address"] + if billing_zip is None: + billing_zip = fake_person["billing_zip"] + + cc = self.make_card( + token_url=token_url, + first_name=first_name, + last_name=last_name, + address=address, + billing_zip=billing_zip, + ) + + return cc + + def get_credit_card(self, id: str) -> CreditCard: + """Abstract method to get a virtual credit card""" + pass + + def get_transaction_events( # noqa C901 # pragma: no cover + self, since: Optional[datetime.datetime] = None + ) -> list: + """Returns a list of recent transactions for the org""" + txns = [] + req = self._get_api("https://api.paywithextend.com/events") + for event in req.json().get("events"): + if since is not None: + if since > datetime.datetime.fromisoformat( + event["timestamp"].split("+")[0] + ): + # We've gone far enough back to not need to continue + return txns + if "transaction" in event["type"]: + txns.append(event["data"]) + if req.json().get("pagination")["numberOfPages"] > 1: + page = 1 + while page < req.json().get("pagination")["numberOfPages"]: + req = self._get_api( + "https://api.paywithextend.com/events?page={0}".format(str(page)) + ) + for event in req.json().get("events"): + if since is not None: + if since > datetime.datetime.fromisoformat( + event["timestamp"].split("+")[0] + ): + # We've gone far enough back to not need to continue + return txns + if "transaction" in event["type"]: + txns.append(event["data"]) + page += 1 + return txns + + def subscribe_to_txns(self, url: str): # pragma: no cover + """Adds a subscription to send transaction events to the passed webhook url""" + parent_cc = self.get_parent_card_id() + events = [ + "transaction.authorized", + "transaction.declined", + "transaction.reversed", + "transaction.settled", + "transaction.updated.no_match", + ] + body = {"creditCardId": parent_cc, "enabledEvents": events, "url": url} + req = self._post_api("https://api.paywithextend.com/subscriptions", body) + return req.json() + + def delete_subscription(self, sub_id): # pragma: no cover + self._delete_api("https://api.paywithextend.com/subscriptions/" + sub_id) + + def get_transaction_info_from_event(self, eventid: str): # pragma: no cover + """Returns the virtual card ID from a transaction event""" + res = self._get_api("https://api.paywithextend.com/events/" + eventid) + return res.json().get("event", {}).get("data", {}) + + def issue_test_transaction( + self, cc: CreditCard, amount: int = 1000 + ) -> None: # pragma: no cover + """Issues a test transaction to the passed card""" + return None # This API does not work for "real" cards + data = {"amount": amount, "type": "DECLINED"} + req = self._post_api( + "https://api.paywithextend.com/virtualcards/{0}/transactions/simulate".format( + cc.id + ), + data=data, + ) + if req.status_code != 200: + print("Error issuing test transaction to: " + cc.id) + return req diff --git a/canarytokens/models.py b/canarytokens/models.py index 289ec178c..19ae5e923 100644 --- a/canarytokens/models.py +++ b/canarytokens/models.py @@ -1,4 +1,6 @@ from __future__ import annotations +from abc import ABCMeta, abstractmethod +import csv import enum import json @@ -8,8 +10,9 @@ from dataclasses import dataclass from datetime import datetime from distutils.util import strtobool +from fastapi.responses import JSONResponse from functools import cached_property -from io import BytesIO +from io import BytesIO, StringIO from ipaddress import IPv4Address from tempfile import SpooledTemporaryFile from typing import ( @@ -50,6 +53,19 @@ re.IGNORECASE, ) +response_error = lambda error, message: JSONResponse( # noqa: E731 # lambda is cleaner + { + "error": str(error), + "error_message": message, + "url": "", + "url_components": None, + "token": "", + "email": "", + "hostname": "", + "auth": "", + } +) + class Memo(ConstrainedStr): max_length: int = MEMO_MAX_CHARACTERS @@ -153,6 +169,99 @@ class KubeCerts(TypedDict): k: bytes # Key +class CreditCard(BaseModel): + id: str + number: Optional[str] + cvc: Optional[str] + expiration: Optional[str] + kind: Optional[str] + name: str + billing_zip: str + address: str + + def render_html(self) -> str: + """Returns an HTML div to render the card info on a website""" + # return """
{name}{number}{expiration}{cvc}
""".format( + # kind=self.kind, + # cvc=self.cvc, + # number=self.number, + # name=self.name, + # expiration=self.expiration, + # ) + return f"""
{self.name}{self.__format_token()}{self.expiration}{self.cvc}
""" + + def to_csv(self) -> str: + f = StringIO() + fn = ["name", "type", "number", "cvc", "exp", "billing_zip"] + sd = self.to_dict() + del sd["address"] + del sd["id"] + writer = csv.DictWriter(f, fieldnames=fn) + writer.writeheader() + writer.writerow(sd) + return f.getvalue() + + def to_dict(self) -> Dict[str, str]: + """Returns the CC information as a python dict""" + out = { + "id": str(self.id), + "name": self.name, + "number": str(self.number), + "cvc": str(self.cvc), + "billing_zip": str(self.billing_zip), + "type": str(self.kind), + "address": str(self.address), + "exp": str(self.expiration), + } + return out + + def __format_token(self): + digits = 4 + if self.kind != "AMEX": + split = [ + self.number[i : i + digits] # noqa: E203 + for i in range(0, len(self.number), digits) + ] + return " ".join(split) + else: + split = [self.number[0:4], self.number[4:10], self.number[10:15]] + return " ".join(split) + + +class ApiProvider(metaclass=ABCMeta): + """Abstract base class for a credit card API provider""" + + def __init__(self): + pass + + @abstractmethod + def create_credit_card( + self, + token_url: str, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + address: Optional[str] = None, + billing_zip: Optional[str] = None, + ) -> CreditCard: + """Abstract method to create a virtual credit card number""" + pass + + @abstractmethod + def get_credit_card(self, id: str) -> CreditCard: + """Abstract method to get a virtual credit card""" + pass + + @abstractmethod + def get_latest_transaction(self, cc: CreditCard) -> Optional[Dict[str, str]]: + """Abstract method to get the latest transaction for a credit card""" + pass + + +# class CCToken(object): +# def __init__(self, api_provider: ApiProvider): +# pass + + class TokenTypes(str, enum.Enum): """Enumerates all supported token types""" @@ -177,6 +286,7 @@ class TokenTypes(str, enum.Enum): KUBECONFIG = "kubeconfig" LOG4SHELL = "log4shell" CMD = "cmd" + CC = "cc" def __str__(self) -> str: return str(self.value) @@ -337,6 +447,10 @@ def check_process_name(value: str): return value +class CCTokenRequest(TokenRequest): + token_type: Literal[TokenTypes.CC] = TokenTypes.CC + + class KubeconfigTokenRequest(TokenRequest): token_type: Literal[TokenTypes.KUBECONFIG] = TokenTypes.KUBECONFIG @@ -479,6 +593,7 @@ class WindowsDirectoryTokenRequest(TokenRequest): AnyTokenRequest = Annotated[ Union[ + CCTokenRequest, CMDTokenRequest, FastRedirectTokenRequest, QRCodeTokenRequest, @@ -565,6 +680,17 @@ class CMDTokenResponse(TokenResponse): reg_file: str +class CCTokenResponse(TokenResponse): + token_type: Literal[TokenTypes.CC] = TokenTypes.CC + kind: str + number: str + cvc: str + expiration: str + name: str + billing_zip: str + rendered_html: str + + class QRCodeTokenResponse(TokenResponse): token_type: Literal[TokenTypes.QR_CODE] = TokenTypes.QR_CODE qrcode_png: str @@ -746,6 +872,7 @@ class MySQLTokenResponse(TokenResponse): AnyTokenResponse = Annotated[ Union[ + CCTokenResponse, CMDTokenResponse, CustomImageTokenResponse, SMTPTokenResponse, @@ -1090,6 +1217,13 @@ class PDFTokenHit(TokenHit): token_type: Literal[TokenTypes.ADOBE_PDF] = TokenTypes.ADOBE_PDF +class CCTokenHit(TokenHit): + token_type: Literal[TokenTypes.CC] = TokenTypes.CC + last4: str + amount: str + merchant: str + + class CMDTokenHit(TokenHit): token_type: Literal[TokenTypes.CMD] = TokenTypes.CMD @@ -1201,6 +1335,7 @@ class WireguardTokenHit(TokenHit): AnyTokenHit = Annotated[ Union[ + CCTokenHit, CMDTokenHit, DNSTokenHit, AWSKeyTokenHit, @@ -1318,6 +1453,11 @@ class PDFTokenHistory(TokenHistory[PDFTokenHit]): hits: List[PDFTokenHit] +class CCTokenHistory(TokenHistory[CCTokenHit]): + token_type: Literal[TokenTypes.CC] = TokenTypes.CC + hits: List[CCTokenHit] + + class CMDTokenHistory(TokenHistory[CMDTokenHit]): token_type: Literal[TokenTypes.CMD] = TokenTypes.CMD hits: List[CMDTokenHit] @@ -1456,6 +1596,7 @@ class SvnTokenHistory(TokenHistory[SvnTokenHit]): # TokenHistory where they differ only in `token_type`. AnyTokenHistory = Annotated[ Union[ + CCTokenHistory, CMDTokenHistory, DNSTokenHistory, AWSKeyTokenHistory, @@ -1679,6 +1820,7 @@ class DownloadFmtTypes(str, enum.Enum): MYSQL = "my_sql" QRCODE = "qr_code" CMD = "cmd" + CC = "cc" def __str__(self) -> str: return str(self.value) @@ -1749,6 +1891,10 @@ class DownloadCMDRequest(TokenDownloadRequest): fmt: Literal[DownloadFmtTypes.CMD] = DownloadFmtTypes.CMD +class DownloadCCRequest(TokenDownloadRequest): + fmt: Literal[DownloadFmtTypes.CC] = DownloadFmtTypes.CC + + class DownloadKubeconfigRequest(TokenDownloadRequest): fmt: Literal[DownloadFmtTypes.KUBECONFIG] = DownloadFmtTypes.KUBECONFIG @@ -1760,6 +1906,7 @@ class DownloadSplackApiRequest(TokenDownloadRequest): AnyDownloadRequest = Annotated[ Union[ DownloadAWSKeysRequest, + DownloadCCRequest, DownloadCMDRequest, DownloadIncidentListCSVRequest, DownloadIncidentListJsonRequest, @@ -1857,6 +2004,15 @@ class DownloadIncidentListCSVResponse(TokenDownloadResponse): auth: str +class DownloadCCResponse(TokenDownloadResponse): + contenttype: Literal[ + DownloadContentTypes.TEXTPLAIN + ] = DownloadContentTypes.TEXTPLAIN + filename: str + token: str + auth: str + + class DownloadCMDResponse(TokenDownloadResponse): contenttype: Literal[ DownloadContentTypes.TEXTPLAIN diff --git a/canarytokens/msword.py b/canarytokens/msword.py index 56ae91c09..95d34d3d0 100644 --- a/canarytokens/msword.py +++ b/canarytokens/msword.py @@ -14,7 +14,6 @@ def make_canary_msword(url: str, template: Path): - with open(template, "rb") as f: input_buf = BytesIO(f.read()) output_buf = BytesIO() diff --git a/canarytokens/pdfgen.py b/canarytokens/pdfgen.py index b64f05d6f..37a0dbd40 100644 --- a/canarytokens/pdfgen.py +++ b/canarytokens/pdfgen.py @@ -49,7 +49,6 @@ def _substitute_stream( def make_canary_pdf( hostname: bytes, template: Path, stream_offset: int = STREAM_OFFSET ): - with open(template, "rb") as fp: contents = fp.read() diff --git a/canarytokens/queries.py b/canarytokens/queries.py index 677384d9b..a461dcfab 100644 --- a/canarytokens/queries.py +++ b/canarytokens/queries.py @@ -45,7 +45,6 @@ def get_canarydrop(canarytoken: tokens.Canarytoken) -> Optional[cand.Canarydrop]: - canarydrop: dict = DB.get_db().hgetall(KEY_CANARYDROP + canarytoken.value()) if len(canarydrop) == 0: @@ -466,6 +465,7 @@ def get_return_for_token(): # return User(DB.get_db().hgetall(account_key)) + # TODO: add counter's / metrics so it's easy to consume. def lookup_canarytoken_alert_count(canarytoken: tokens.Canarytoken) -> int: key = KEY_CANARYTOKEN_ALERT_COUNT + canarytoken.value() @@ -564,9 +564,9 @@ def put_mail_on_sent_queue(mail_key: str, details: models.TokenAlertDetails) -> return DB.get_db().lpush(KEY_SENT_MAIL_QUEUE, sent_mail) -def pop_mail_off_sent_queue() -> tuple[ - Optional[str], Optional[models.TokenAlertDetails] -]: +def pop_mail_off_sent_queue() -> ( + tuple[Optional[str], Optional[models.TokenAlertDetails]] +): item = DB.get_db().rpop(KEY_SENT_MAIL_QUEUE, count=1) if item is None: log.info(f"No mail to send on queue: {KEY_SENT_MAIL_QUEUE}") @@ -918,7 +918,6 @@ def is_email_blocked(email): def is_tor_relay(ip): - if not DB.get_db().exists(KEY_TOR_EXIT_NODES): update_tor_exit_nodes_loop() # FIXME: DESIGN: we call defered and expect a result in redis, Now! return DB.get_db().sismember(KEY_TOR_EXIT_NODES, json.dumps(ip)) diff --git a/canarytokens/settings.py b/canarytokens/settings.py index f9474e777..e77f6b4ce 100644 --- a/canarytokens/settings.py +++ b/canarytokens/settings.py @@ -8,7 +8,6 @@ class Settings(BaseSettings): - CHANNEL_DNS_PORT: Port = Port(5354) CHANNEL_HTTP_PORT: Port = Port(8083) CHANNEL_SMTP_PORT: Port = Port(2500) @@ -63,7 +62,6 @@ class Config: class FrontendSettings(BaseSettings): - API_APP_TITLE: str = "Canarytokens" API_VERSION_STR: str = "v1" FRONTEND_HOSTNAME: str @@ -90,6 +88,9 @@ class FrontendSettings(BaseSettings): # 3rd party settings GOOGLE_API_KEY: str + EXTEND_EMAIL: Optional[str] + EXTEND_PASSWORD: Optional[SecretStr] = SecretStr("NoExtendPasswordFound") + EXTEND_CARD_NAME: Optional[str] class Config: allow_mutation = False diff --git a/canarytokens/tokens.py b/canarytokens/tokens.py index b43731e91..2f267e797 100644 --- a/canarytokens/tokens.py +++ b/canarytokens/tokens.py @@ -414,6 +414,24 @@ def _get_info_for_clonedsite(request): } return http_general_info, src_data + @staticmethod + def _get_info_for_cc(request): + http_general_info = Canarytoken._grab_http_general_info(request=request) + + last4 = request.getHeader("Last4") + amount = "$" + request.getHeader("Amount") + merchant = request.getHeader("Merchant") + + # TODO: check if we need to nerf geo_info, src_ip and is_tor_relay + # from http_general_info + src_data = {"last4": last4, "amount": amount, "merchant": merchant} + return http_general_info, src_data + + @staticmethod + def _get_response_for_cc(canarydrop: canarydrop.Canarydrop, request: Request): + request.setHeader("Content-Type", "image/gif") + return GIF + @staticmethod def _get_response_for_clonedsite( canarydrop: canarydrop.Canarydrop, request: Request @@ -512,7 +530,6 @@ def _get_info_for_web_image(request): def _get_response_for_web_image( canarydrop: canarydrop.Canarydrop, request: Request ): - if request.getHeader("Accept") and "text/html" in request.getHeader("Accept"): if canarydrop.browser_scanner_enabled: # set response mimetype diff --git a/frontend/app.py b/frontend/app.py index 2dfa834c1..8d45fc113 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -33,7 +33,7 @@ from sentry_sdk.integrations.redis import RedisIntegration import canarytokens -from canarytokens import kubeconfig, msreg, queries +from canarytokens import extendtoken, kubeconfig, msreg, queries from canarytokens import wireguard as wg from canarytokens.authenticode import make_canary_authenticode_binary from canarytokens.awskeys import get_aws_key @@ -46,6 +46,8 @@ AnyTokenResponse, AWSKeyTokenRequest, AWSKeyTokenResponse, + CCTokenRequest, + CCTokenResponse, ClonedWebTokenRequest, ClonedWebTokenResponse, CMDTokenRequest, @@ -58,6 +60,8 @@ DNSTokenResponse, DownloadAWSKeysRequest, DownloadAWSKeysResponse, + DownloadCCRequest, + DownloadCCResponse, DownloadCMDRequest, DownloadCMDResponse, DownloadIncidentListCSVRequest, @@ -96,6 +100,7 @@ PDFTokenResponse, QRCodeTokenRequest, QRCodeTokenResponse, + response_error, SettingsResponse, SlowRedirectTokenRequest, SlowRedirectTokenResponse, @@ -300,20 +305,6 @@ async def generate(request: Request) -> AnyTokenResponse: # noqa: C901 # gen i """ Whatt """ - response_error = ( - lambda error, message: JSONResponse( # noqa: E731 # lambda is cleaner - { - "error": str(error), - "error_message": message, - "url": "", - "url_components": None, - "token": "", - "email": "", - "hostname": "", - "auth": "", - } - ) - ) if request.headers.get("Content-Type", "application/json") == "application/json": token_request_data = await request.json() @@ -549,6 +540,19 @@ def _( ) +@create_download_response.register +def _( + download_request_details: DownloadCCRequest, canarydrop: Canarydrop +) -> DownloadCCResponse: + """""" + return DownloadCCResponse( + token=download_request_details.token, + auth=download_request_details.auth, + content=canarydrop.cc_rendered_csv, + filename=f"{canarydrop.canarytoken.value()}.csv", + ) + + @create_download_response.register def _( download_request_details: DownloadMSWordRequest, canarydrop: Canarydrop @@ -968,6 +972,54 @@ def _( ) +@create_response.register +def _(token_request_details: CCTokenRequest, canarydrop: Canarydrop) -> CCTokenResponse: + eapi = extendtoken.ExtendAPI( + email=frontend_settings.EXTEND_EMAIL, + password=frontend_settings.EXTEND_PASSWORD.get_secret_value(), + card_name=frontend_settings.EXTEND_CARD_NAME, + ) + try: + cc = eapi.create_credit_card(token_url=canarydrop.token_url) + except extendtoken.ExtendAPIRateLimitException: + return response_error( + 4, "Credit Card Rate-Limiting currently in place. Please try again later." + ) + + if not cc or not cc.number: + return response_error( + 4, "Failed to generate credit card. Please contact support@thinkst.com." + ) + canarydrop.cc_kind = cc.kind + canarydrop.cc_number = cc.number + canarydrop.cc_cvc = cc.cvc + canarydrop.cc_expiration = cc.expiration + canarydrop.cc_name = cc.name + canarydrop.cc_billing_zip = cc.billing_zip + canarydrop.cc_rendered_html = cc.render_html() + canarydrop.cc_rendered_csv = cc.to_csv() + queries.save_canarydrop(canarydrop=canarydrop) + + return CCTokenResponse( + email=canarydrop.alert_email_recipient or "", + webhook_url=canarydrop.alert_webhook_url + if canarydrop.alert_webhook_url + else "", + token=canarydrop.canarytoken.value(), + token_url=canarydrop.get_url([canary_http_channel]), + auth_token=canarydrop.auth, + hostname=canarydrop.get_hostname(), + url_components=list(canarydrop.get_url_components()), + kind=canarydrop.cc_kind, + number=canarydrop.cc_number, + cvc=canarydrop.cc_cvc, + expiration=canarydrop.cc_expiration, + name=canarydrop.cc_name, + billing_zip=canarydrop.cc_billing_zip, + rendered_html=canarydrop.cc_rendered_html, + ) + + @create_response.register def _(token_request_details: PDFTokenRequest, canarydrop: Canarydrop): return PDFTokenResponse( diff --git a/poetry.lock b/poetry.lock index d17d03fb1..9c3488d52 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. [[package]] name = "anyio" @@ -1026,6 +1026,21 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "faker" +version = "17.6.0" +description = "Faker is a Python package that generates fake data for you." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Faker-17.6.0-py3-none-any.whl", hash = "sha256:5aaa16fa9cfde7d117eef70b6b293a705021e57158f3fa6b44ed1b70202d2065"}, + {file = "Faker-17.6.0.tar.gz", hash = "sha256:51f37ff9df710159d6d736d0ba1c75e063430a8c806b91334d7794305b5a6114"}, +] + +[package.dependencies] +python-dateutil = ">=2.4" + [[package]] name = "fastapi" version = "0.79.1" @@ -2269,7 +2284,7 @@ pyside2 = ["qt5reactor[pyside2] (>=0.6.3)"] name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" -category = "dev" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -3057,9 +3072,9 @@ test = ["zope.i18nmessageid", "zope.testing", "zope.testrunner"] [extras] twisted = ["Twisted", "sentry-sdk"] -web = ["fastapi", "uvicorn", "sentry-sdk"] +web = ["fastapi", "sentry-sdk", "uvicorn"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "663d851a51ed0cc8777d5eb0c04440845ea0726bbece79ea477bff0ea1d58ef3" +content-hash = "d825d7d01691c6b04ca88b5eed88e58bd786e37f762e87bbf939e55e707300c0" diff --git a/pyproject.toml b/pyproject.toml index a24d965d7..61579d927 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ fastapi = { version = "^0.79.0", optional = true } sentry-sdk = {extras = ["fastapi"], version = "^1.9.5", optional = true} uvicorn = { version = "^0.17.6", optional = true } Twisted = {version = "^22.4.0", optional = true} +faker = "^17.6.0" [tool.poetry.extras] web = ["fastapi", "uvicorn", "sentry-sdk"] diff --git a/templates/emails/notification.html b/templates/emails/notification.html index 6910cd5c2..84b600707 100644 --- a/templates/emails/notification.html +++ b/templates/emails/notification.html @@ -125,6 +125,24 @@

Basic Details:

{{ BasicDetails['Log4_shell_computer_name'] | e}} {% endif %} + {% if BasicDetails['CMDInformation'] %} + + Sensitive Command Information + {{ BasicDetails['CMDInformation'] | e}} + + {% endif %} + {% if BasicDetails['Merchant'] %} + + Authorizing merchant + {{ BasicDetails['Merchant'] | e}} + + {% endif %} + {% if BasicDetails['Amount'] %} + + Transaction amount + {{ BasicDetails['Amount'] | e}} + + {% endif %} {% if BasicDetails['CanaryIP'] or BasicDetails['CanaryName'] %} Canary diff --git a/templates/generate_new.html b/templates/generate_new.html index f80de23e3..714659fce 100644 --- a/templates/generate_new.html +++ b/templates/generate_new.html @@ -31,6 +31,20 @@ Copy to clipboard {%- endmacro %} + +{% macro textareacopydownload(name, download_fmt, value="")%} + +
+ + + Download text area + +
+{%- endmacro %} + @@ -59,7 +73,8 @@ - + + \ No newline at end of file diff --git a/templates/static/credit-card.png b/templates/static/credit-card.png new file mode 100644 index 0000000000000000000000000000000000000000..ed5635457924d3f2dfa9d4140b526eccb0891742 GIT binary patch literal 54429 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 literal 0 HcmV?d00001 diff --git a/templates/static/download.svg b/templates/static/download.svg new file mode 100644 index 000000000..cfc21ae85 --- /dev/null +++ b/templates/static/download.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/templates/static/msreg.png b/templates/static/msreg.png new file mode 100644 index 0000000000000000000000000000000000000000..7a8332994e7d3fbaad87f847448537d8e14cce8b GIT binary patch literal 7324 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
cPsAxX1d+{)OA^ zb_$gYJLFjEuq6IRO;dh%ST$;WuSeN#rmL3=*E1v1sz;~ImDLn>l~AtQPP!HrD2s+B zab8N5C`eX(;&rvY6Zeu#J(<=tdbrRG`TftpS#=(2te^|MA2KwI@ayiZq{uEIcGH0l z8+Xmw9t$QRZ-+epQHvJhu&NnZs0x9&itL} zO~XTDa?SadC_uFqEW1)>SW)0p2)53C@^9)P-32B@TO1~EHsJQh?aH~Tg*M?&rM3oA zd3>$Tw5#S>?E1`zCo83KdLgtMt2H^p1ou`GCsCw(CWXA|cCp{7ygwVdyRJ2Xs!^U( zU~TE?UH8%?5@lC}#t+SDow)1`qJ1T|<=8u-Nk}lg>fU(OqCBjZ5w0NTU{55!h<6Cs z@ZNcHF|T~wh8wsGHDlz!DYk|jzgMoMh3H;sY7K3>fB(3)50F6C!TuZB9SfozMZn+^ zeb^J=4OJcsHUu~2ls!~qgplE@UY^L$^Lc4ov4-dT!be@0-aQRhVJ4XO$LWkK6iNS@A z&*%s6*EL?rwoMHNM9ZG3p?y}mxWy-d6d$PJ?{mejqkyPB2SOCLE?1(w?7AsTr`~<7 zzNx4O^9gO=N7o8m@mL4@sYIJ9x)L5bID3OL3Ou%ez#x9$5*I=mXN3mR0#NZ!O~)EQ z&4Fh-D0{VtPApv5p{9qd^H4yspFV6Z;jIi#)i2^pN{KX19}Rd+kzuZq5u5XN=NhIS zLn47ZQpc8PUXbRlU^k_^=2mSF^u!M4;2Z0osOX0JM~nvU#aQ3vEn@teq^us3`Eaeb zLCoH_6P~7t=|n@eqNrL<)kA-W<3<}u2T4k`^D#%cW(-W!{TNmpKFi3T?6w=2eEC0H z>Hlfa8pIYnZ{V$17(RBwGy(=EoOuJiUC>#mYck&tOpe084GL8A7T0Lo3VJ1%Zj}!I@`5xjq2`mO>eW#jHRio$u$(USi*Oh`TmLl! z6a-9Q_WcT8xrw7&-#th*Xe40lr4PFrsqOW|kssK9=#a7-z-7QNJ5rGvvoCsN$@R;b zw^v0|{sDm1g)ga>!uRW^E1=H_(cuF;l!Zq&N9#Rw>cr-Co-5jRN1mJzpt)XgGf$&F zl}vl=3p~Ul@Ea{h?hxQjyYa#b>4JF)1>(nlA2{g z6OadNYI94K1KAGBurR?t23i$|5^%>&xr+mDDMM zK@`F%NhGyw;b$t;G@=lL*&?a+(4TYvdt!W=PZC#^&j1vjJD`77I~$-dU5)I|T0$iNMDVvm&GqMB9Mt>BlDm0W%UWYUTpv zr)o5;ch)@|YSm{R5$h{^&WZ@d-4hk~|4o|yZ`A8B#DG3r>Q*f8Iq0h+(APH9`f%C) G@&5ty8=u<% literal 0 HcmV?d00001 diff --git a/templates/static/styles.css b/templates/static/styles.css index 11b4834e4..12ed27e3e 100644 --- a/templates/static/styles.css +++ b/templates/static/styles.css @@ -14,6 +14,12 @@ border-bottom: 2px solid #fff; padding: 1rem 0rem; } +.goodtick { + height: 100px; + margin: auto; + margin-top: 20px; + margin-bottom: 20px; +} .error-outline { outline: none; @@ -308,6 +314,22 @@ input.form-control:placeholder, textarea.form-control:placeholder { overflow: scroll; } +input#search-token { + width: 100%; + height: 45px; + border: none; + border-bottom: 1px solid #eceeef; + text-align: center; + color: #888888; +} +input#search-token:focus, input#search-token:focus-visible { + outline: #eceeef; +} + +input#search-token::placeholder { + color: #c7c7c7; +} + .explanation { font-size: smaller; color: #999; @@ -379,6 +401,7 @@ a.btn-success.btn { background-color: #38d47f; width: 100%; border-color: #38d47f; + color: white; } button.btn.btn-success:disabled { opacity: .85; @@ -423,6 +446,7 @@ a.btn-success.btn:hover, a.btn-success.btn:focus { .result-data { width: 80%; + min-height: 100px; border: none; font-family: monospace; padding: 5px 12px; @@ -500,7 +524,17 @@ a.btn-success.btn:hover, a.btn-success.btn:focus { background: white; border: none; outline: none; - height: 20px; + height: 22px; + width: 22px; + background-image: url(/resources/copy-inactive.svg); + background-size: 22px; + background-repeat: no-repeat; + background-position: center; +} + +.copy-card .btn-clipboard.hide-button:hover { + background-image: url(/resources/copy-hover.svg); + cursor: pointer; } #ccRender { @@ -678,6 +712,34 @@ a.btn-success.btn:hover, a.btn-success.btn:focus { margin-top: 2rem; } +.advice li { + list-style: none; + position: relative; + margin: 20px 0 20px 0; + font-size: 15px; + /* color: #666666; */ +} +.advice li::before { + content: ''; + background-image: url('/resources/idea.svg'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + background-size: 2em; + height: 100%; + width: 30px; + position: absolute; + left: -40px; +} + +@media screen and (max-width: 600px) { + .advice li { + margin: 10px 0 10px 0; + font-size: 14px; + } + } + + .create-link { text-align: center; margin-bottom: 15px; @@ -747,13 +809,13 @@ a.refresh:hover{ .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary { color: #fff; - background-color: #5cb85c; - border-color: #5cb85c; + background-color: #38d47f; + border-color: #38d47f; } .jumbotron .btn.btn-clipboard { - background-color: #5cb85c; - border-color: #5cb85c; + background-color: #38d47f; + border-color: #38d47f; color: #fff; } @@ -848,6 +910,9 @@ a.icon-svn:before { a.icon-aws:before { background-image: url("/resources/aws.png"); } +a.icon-azureid:before { + background-image: url("/resources/azure-id.png"); +} a.icon-redirect:before { background-image: url("/resources/redirect.png"); } @@ -984,4 +1049,4 @@ a:focus.what-is-this, a:hover.what-is-this { a.what-is-this svg { width: 19px; -} \ No newline at end of file +} diff --git a/templates/static/styles.min.css b/templates/static/styles.min.css index 6f72ef8b4..51d68e3fa 100644 --- a/templates/static/styles.min.css +++ b/templates/static/styles.min.css @@ -1 +1 @@ - .logo {height: 32px;}.header a {color: #38d47f;}.hidden {display: none!important;}.step {border-bottom: 2px solid #fff;padding: 1rem 0rem;}.error-outline {outline: none;border-color: #1ecaed;box-shadow: 0 0 13px #ec6161;}.success-outline {outline: none;border-color: #1ecaed;box-shadow: 0 0 10px #38d47f;}a.fileupload-clear:hover {text-decoration: none;}.jumbotron {padding: 0;position: relative;}.create {z-index: 1;position: relative;padding: 2rem 1rem;}.create-hidden {transition:opacity 0.5s ease;display: none;}.history-header {text-align:center;padding: 2rem 1rem;margin-bottom: 2rem;background-color: #eceeef;border-radius: .3rem;}.details-header{padding:0.7rem;text-align: center;}.footer {text-align: center;}.footer .banner {display: block;max-width: 46rem;margin: auto;margin-bottom: 20px;height: 210px;background-image: url('/resources/thinkst-canary-banner-hi-res-2.png');background-repeat: no-repeat;background-position: center;background-size: contain;}.footer a {color: #38d47f;}@media screen and (max-width: 425px) {.footer {padding: 0;font-size: 14px;}.footer p {margin-bottom: 10px;}.footer .banner {background-image: url('/resources/thinkst-canary-banner-mobile-2.png');height: 300px;margin-top: 10px;}}@media screen and (max-width: 375px) {.footer .banner {height: 335px;}}.incident-size{margin :0px auto;display: none;}.incident-list{overflow: auto;position: relative;margin-bottom: 10px;margin-top: 10px;}.incident-item{width: 100%;transition:opacity 0.5s ease;background-color: aliceblue;color: darkslategrey;border-radius: .3rem;margin-bottom: 0.5rem;cursor: pointer;}.incident-item-details{display:none;padding:1rem;}.incident-item:hover{outline: none;border-color: #1ecaed;box-shadow: 0 0 10px green;}.header_row{font-weight:bold;}.incident-item:first-child{margin-top:0.5rem;}.success {width: 100%;z-index: 0;transition:opacity 0.5s ease;background-color: #eceeef;color: darkslategrey;padding-top: 1rem;padding-bottom: 2rem;border-radius: .3rem;display: none;}.success-visible {z-index: 2;display: block;}.results {padding-top: 1rem;}input.form-control, input.fileupload, textarea {margin-top: 10px;margin-bottom: 10px;text-align: center;}input.form-control::-webkit-input-placeholder, textarea.form-control::-webkit-input-placeholder {color: #bbb;}input.form-control:-moz-placeholder, textarea.form-control:-moz-placeholder{color: #bbb;}input.form-control::-moz-placeholder, textarea.form-control::-moz-placeholder {color: #bbb;}input.form-control:placeholder, textarea.form-control:placeholder {color: #bbb;}.form-control {border: 2px solid #dadada;border-radius: 7px;}.form-control:focus {outline: none;border-color: #dadada;box-shadow: 0 0 14px #4a4a4a;}.step:last-child {border-bottom: none;padding-bottom: 0rem;}.wrapper-dropdown {position: relative;margin: 0 auto;margin-bottom: 10px;padding: 12px 15px;background: #fff;border-radius: 5px;cursor: pointer;outline: none;transition: all 0.3s ease-out;}.wrapper-dropdown:after {content: "";width: 0;height: 0;position: absolute;top: 50%;right: 15px;margin-top: -3px;border-width: 6px 6px 0 6px;border-style: solid;border-color: #38d47f transparent;}.success-outline.wrapper-dropdown:after {border-color: green transparent;}.wrapper-dropdown .dropdown {position: absolute;top: 100%;left: 0;right: 0;background: #fff;border-radius: 0 0 5px 5px;border: 1px solid rgba(0,0,0,0.2);border-top: none;border-bottom: none;list-style: none;transition: all 0.3s ease-out;padding-left: 0px;max-height: 0;overflow: hidden;z-index: 999;}.wrapper-dropdown .dropdown li {}.wrapper-dropdown .dropdown li a {display: block;text-decoration: none;color: #333;padding: 10px 0;transition: all 0.3s ease-out;border-bottom: 1px solid #e6e8ea;}.wrapper-dropdown .dropdown li a:hover {color: #38d47f !important;}.wrapper-dropdown .dropdown li:last-of-type a {border: none;}.wrapper-dropdown .dropdown li i {margin-right: 5px;color: inherit;vertical-align: middle;}.wrapper-dropdown .dropdown li:hover a {color: #57a9d9;background-color: #eee;}.wrapper-dropdown.active {border-radius: 5px 5px 0 0;background: #38d47f;box-shadow: none;border-bottom: 2px solid #eceeef;color: white;border: none;}.wrapper-dropdown.active:after {border-color: #FFF transparent;}.wrapper-dropdown.active .dropdown {border-bottom: 1px solid rgba(0,0,0,0.2);max-height: 400px;overflow: scroll;}.explanation {font-size: smaller;color: #999;}.fileupload-wrapper {display: block;position: relative;cursor: pointer;overflow: hidden;width: 100%;max-width: 100%;padding: 5px 10px;margin-top:15px;font-size: 14px;line-height: 22px;color: #777;background-color: #FFF;background-image: none;text-align: center;border: 2px solid #E5E5E5;-webkit-transition: border-color .15s linear;transition: border-color .15s linear;}.fileupload-wrapper .fileupload-message {position: relative;-webkit-transform: translateY(35%);transform: translateY(35%);text-align: center;font-family: sans-serif;font-size: 16px;color: #bbb;}.fileupload-wrapper input {position: absolute;top: 0;right: 0;bottom: 0;left: 0;height: 100%;width: 100%;opacity: 0;cursor: pointer;z-index: 5;}.fileupload-wrapper .fileupload-filename {font-size: 16px;position: relative;transform: translateY(35%);}.fileupload-wrapper p {padding-bottom: 0px;}#create-token-p{margin-top: 1rem;margin-bottom: 0rem;}#save.btn-fullwidth {width: 100%;background-color: #38d47f;}a.btn-success.btn {background-color: #38d47f;width: 100%;border-color: #38d47f;}button.btn.btn-success:disabled {opacity: .85;}@media (min-width: 500px) {#save.btn-fullwidth {font-size: 2rem;}}#save.btn-disabled {font-size: 1rem;width: 14rem;background-color: #ec6161;border: none;}a.btn-success.btn:hover, a.btn-success.btn:focus {color: #fff;}.result {display: none;}#result_cloned_website {height: 10rem;}.jumbotron .btn.btn-clipboard {background-color: #9d8;padding: 5px .5rem;font-size: inherit;cursor: pointer;margin-left: -5px;border-top-left-radius: 0;border-bottom-left-radius: 0;}.btn-clipboard > img {max-width: 15px;height: 15px;}.result-data {width: 80%;border: none;font-family: monospace;padding: 5px 12px;font-size: 17px;line-height: 20px;color: #24292e;vertical-align: middle;background-color: #ffffff;background-repeat: no-repeat;background-position: right 8px center;border: 1px solid #e1e4e8;border-radius: 6px;outline: none;box-shadow: rgba(225, 228, 232, 0.2) 0px 1px 0px 0px inset;border-top-right-radius: 0;border-bottom-right-radius: 0;}.result-data:focus{outline: none;box-shadow: #01ac4b 0px 0px 0px 1px;}.credit-card {display: flex;justify-content: space-evenly;margin-bottom: 15px;}.copy-card .card-detail {display: flex;align-items: flex-end;}.copy-card .card-detail div {display: flex;flex-direction: column;margin-left: 10px;align-items: flex-start;}.copy-card .card-detail .title {font-size: 11.5px;margin-bottom: 5px;font-weight: 300;}.copy-card {display: flex;flex-wrap: wrap;flex-direction: column;justify-content: space-around;font-size: 15px;margin: 15px 0 20px 0;}.copy-card #copy-name, .copy-card #copy-card, .copy-card #copy-expiry, .copy-card #copy-cvc {font-family: monospace;color: #646464;font-weight: 600;font-size: 16px;}.copy-card img {width: 17px;height: 17px;margin-bottom: 3px;}.copy-card img:hover {cursor: pointer;border-radius: 50%;border: 1px solid #24b47e;padding: 2px;}.copy-card .btn-clipboard.hide-button {background: white;border: none;outline: none;height: 20px;}#ccRender {margin-top: 15px;margin-bottom: 20px;width: 335px;}#cccontainer {font-family: 'OCR A Extended', sans-serif;position: relative;margin: auto;background-image: url(/resources/cc-background-AMEX.png);height: 205px;background-repeat: no-repeat;background-size: contain;color: white;}#cccontainer #ccname {left: 30px;top: 75px;position: absolute;font-size: 16pt;}#cccontainer #ccnumber {left: 30px;top: 100px;position: absolute;font-size: 14pt;word-spacing: 0.45em;}#cccontainer #ccexpires {left: 25px;top: 160px;position: absolute;font-size: 13pt;}#cccontainer #cccvc {left: 165px;top: 160px;position: absolute;font-size: 13pt;}@media screen and (max-width: 768px) {.credit-card {flex-wrap: wrap;margin-bottom: 0;}.copy-card {display: grid;grid-template-columns: auto auto;column-gap: 20px;row-gap: 20px;}}@media screen and (max-width: 600px) {.copy-card {column-gap: 10px;row-gap: 10px;margin: 10px auto;}.copy-card span {margin-bottom: 3px;display: flex;}.copy-card .card-detail .title {font-size: 11px;text-align: left;}.copy-card #copy-name, .copy-card #copy-card, .copy-card #copy-expiry, .copy-card #copy-cvc {font-size: 11px;text-align: left;}#cccontainer {height: 190px !important;width: auto !important;background-repeat: no-repeat;background-size: contain;}#cccontainer #ccname {left: 25px !important;top: 70px !important;font-size: 15pt !important;}#cccontainer #ccnumber {left: 25px !important;top: 95px !important;font-size: 13pt !important;}#cccontainer #ccexpires {left: 25px !important;top: 145px !important;font-size: 12pt !important;}#cccontainer #cccvc {left: 145px !important;top: 145px !important;font-size: 12pt !important;}}@media screen and (min-width: 375px) and (max-width: 425px) {#cccontainer {height: 160px !important;width: 240px !important;}#cccontainer #ccname {left: 20px !important;top: 60px !important;font-size: 13pt !important;}#cccontainer #ccnumber {left: 20px !important;top: 80px !important;font-size: 10pt !important;}#cccontainer #ccexpires {left: 20px !important;top: 119px !important;font-size: 9pt !important;}#cccontainer #cccvc {left: 120px !important;top: 119px !important;font-size: 9pt !important;}}@media screen and (min-width: 320px) and (max-width: 375px) {.copy-card .card-detail .title {font-size: 9.5px;text-align: left;}.copy-card #copy-name, .copy-card #copy-card, .copy-card #copy-expiry, .copy-card #copy-cvc {font-size: 10px;text-align: left;}.copy-card img {width: 15px;height: 15px;}#cccontainer {height: 130px !important;width: 190px !important;}#cccontainer #ccname {left: 15px !important;top: 45px !important;font-size: 10pt !important;}#cccontainer #ccnumber {left: 15px !important;top: 60px !important;font-size: 9pt !important;}#cccontainer #ccexpires {left: 15px !important;top: 92px !important;font-size: 9pt !important;}#cccontainer #cccvc {left: 95px !important;top: 92px !important;font-size: 9pt !important;}}.advice {text-align: left;margin-top: 2rem;}.create-link {text-align: center;margin-bottom: 15px;}.create-link a {}a.refresh {font-size: 1.5rem;vertical-align: middle;text-decoration: none;margin-right: 5px;}a.refresh:hover{cursor: pointer;}.pre-like {display: block;font-family: monospace;white-space: pre;margin: 1em 0px;word-break: break-all;overflow: scroll;text-align: left;}.error-field {display: none;color: red;border-bottom: 1px solid red;border-radius: 6px;margin-bottom: 24px;}#endpoints_errors {border: 1px solid red;margin: 24px 0px;padding: 0px 5px;}#endpoints_errors pre {color: #eceeef;background: firebrick;}.map{position: relative;margin-bottom: 10px;margin-top: 10px;height:400px;border-radius: 5px;border: 7px solid #dadada;}.setting .key {text-align: right;}.setting .value {text-align: left;}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary:hover, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary:hover {background-color: #449d44;border-color: #419641;}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary {color: #fff;background-color: #5cb85c;border-color: #5cb85c;}.jumbotron .btn.btn-clipboard {background-color: #5cb85c;border-color: #5cb85c;color: #fff;}.setting .key small {font-size: .6rem;color: #999;vertical-align: top;}@media (min-width: 48em) {.container {max-width: 100%;}}.jumbotron {max-width: 46rem;margin: auto;margin-bottom: 2rem;}a.icon:before {display: block;position: absolute;margin-right: 10px;margin-left: 10px;content: "";height: 40px;width: 40px;background-size: contain;-webkit-transition: all 0.4s ease-in-out;-moz-transition: all 0.4s ease-in-out;transition: all 0.4s ease-in-out;-webkit-transform: translate3d(0, 0, 0);}@media (max-width: 768px) {a.icon:before {width: 20px;height: 20px;}}a.icon-web:before {background-image: url("/resources/web.png");}a.icon-dns:before {background-image: url("/resources/dns.png");}a.icon-web-image:before {background-image: url("/resources/web_image.png");}a.icon-email:before {background-image: url("/resources/email.png");}a.icon-word:before {background-image: url("/resources/word.png");}a.icon-cc:before {background-image: url("/resources/credit-card.png");}a.icon-excel:before {background-image: url("/resources/excel.png");}a.icon-pdf:before {background-image: url("/resources/pdf.png");}a.icon-wireguard:before {background-image: url("/resources/wireguard.png");}a.icon-folder:before {background-image: url("/resources/folder.png");}a.icon-exe:before {background-image: url("/resources/exe.png");}a.icon-clonedsite:before {background-image: url("/resources/clonedsite.png");}a.icon-sqlserver:before {background-image: url("/resources/sqlserver.png");}a.icon-mysql:before {background-image: url("/resources/mysql.png");}a.icon-qrcode:before {background-image: url("/resources/qrcode.png");}a.icon-svn:before {background-image: url("/resources/svn.png");}a.icon-aws:before {background-image: url("/resources/aws.png");}a.icon-redirect:before {background-image: url("/resources/redirect.png");}a.icon-kubernetes:before {background-image: url("/resources/kubernetes_icon.png");}a.icon-log4shell:before {background-image: url("/resources/log4j.png");}a.icon-msreg:before {background-image: url("/resources/msreg.png");}@media (max-width: 768px) {input.form-control:-moz-placeholder, textarea.form-control:-moz-placeholder {font-size: .8rem;}input.form-control::-moz-placeholder, textarea.form-control::-moz-placeholder {font-size: .8rem;}input.form-control:placeholder, textarea.form-control:placeholder {font-size: .8rem;}input.form-control::-webkit-input-placeholder, textarea.form-control::-webkit-input-placeholder {font-size: .8rem;}.setting .key {text-align: center;}.setting .value {text-align: center;margin-bottom: 1rem;}.artifacts{margin-left: 10px;margin-right: 10px;}.row.results{margin-left: 10px;margin-right: 10px;}}@media (max-width: 500px) {input.form-control:-moz-placeholder, textarea.form-control:-moz-placeholder{font-size: .5rem;}input.form-control::-moz-placeholder, textarea.form-control::-moz-placeholder {font-size: .5rem;}input.form-control:placeholder, textarea.form-control:placeholder {font-size: .5rem;}input.form-control::-webkit-input-placeholder, textarea.form-control::-webkit-input-placeholder {font-size: .5rem;}.token-length{font-size: 1.2rem;}.setting .key {text-align: center;}.setting .value {text-align: center;margin-bottom: 1rem;}.artifacts{margin-left: 10px;margin-right: 10px;}.row.results{margin-left: 5px;margin-right: 5px;}a.btn.btn-large.btn-success{font-size: 0.9rem;white-space: pre-wrap;}}#mainsite {font-size: smaller;padding-top: 1.5rem;}.mysql-encoded-button {margin-top: 10px;text-align: center;}.mysql-text {text-align: left;}.mysqldownload {cursor: pointer;}#wg_conf {background: #f6f6f6;display: block;white-space: pre;overflow-x: scroll;max-width: 100%;min-width: 100px;padding: 0;text-align: left;border-radius: 8px;padding-bottom: 12px;padding-left: 12px;padding-top: 12px;margin-bottom: 7px;margin-top: 7px;margin-left: 0px;}#wireguard-app {margin-bottom: 16px;}#wireguard-app img {height: 40px;}a:focus.what-is-this, a:hover.what-is-this {text-decoration: none;}a.what-is-this svg {width: 19px;} \ No newline at end of file + .logo {height: 32px;}.header a {color: #38d47f;}.hidden {display: none!important;}.step {border-bottom: 2px solid #fff;padding: 1rem 0rem;}.goodtick {height: 100px;margin: auto;margin-top: 20px;margin-bottom: 20px;}.error-outline {outline: none;border-color: #1ecaed;box-shadow: 0 0 13px #ec6161;}.success-outline {outline: none;border-color: #1ecaed;box-shadow: 0 0 10px #38d47f;}a.fileupload-clear:hover {text-decoration: none;}.jumbotron {padding: 0;position: relative;}.create {z-index: 1;position: relative;padding: 2rem 1rem;}.create-hidden {transition:opacity 0.5s ease;display: none;}.history-header {text-align:center;padding: 2rem 1rem;margin-bottom: 2rem;background-color: #eceeef;border-radius: .3rem;}.details-header{padding:0.7rem;text-align: center;}.footer {text-align: center;}.footer .banner {display: block;max-width: 46rem;margin: auto;margin-bottom: 20px;height: 210px;background-image: url('/resources/thinkst-canary-banner-hi-res-2.png');background-repeat: no-repeat;background-position: center;background-size: contain;}.footer a {color: #38d47f;}@media screen and (max-width: 425px) {.footer {padding: 0;font-size: 14px;}.footer p {margin-bottom: 10px;}.footer .banner {background-image: url('/resources/thinkst-canary-banner-mobile-2.png');height: 300px;margin-top: 10px;}}@media screen and (max-width: 375px) {.footer .banner {height: 335px;}}.incident-size{margin :0px auto;display: none;}.incident-list{overflow: auto;position: relative;margin-bottom: 10px;margin-top: 10px;}.incident-item{width: 100%;transition:opacity 0.5s ease;background-color: aliceblue;color: darkslategrey;border-radius: .3rem;margin-bottom: 0.5rem;cursor: pointer;}.incident-item-details{display:none;padding:1rem;}.incident-item:hover{outline: none;border-color: #1ecaed;box-shadow: 0 0 10px green;}.header_row{font-weight:bold;}.incident-item:first-child{margin-top:0.5rem;}.success {width: 100%;z-index: 0;transition:opacity 0.5s ease;background-color: #eceeef;color: darkslategrey;padding-top: 1rem;padding-bottom: 2rem;border-radius: .3rem;display: none;}.success-visible {z-index: 2;display: block;}.results {padding-top: 1rem;}input.form-control, input.fileupload, textarea {margin-top: 10px;margin-bottom: 10px;text-align: center;}input.form-control::-webkit-input-placeholder, textarea.form-control::-webkit-input-placeholder {color: #bbb;}input.form-control:-moz-placeholder, textarea.form-control:-moz-placeholder{color: #bbb;}input.form-control::-moz-placeholder, textarea.form-control::-moz-placeholder {color: #bbb;}input.form-control:placeholder, textarea.form-control:placeholder {color: #bbb;}.form-control {border: 2px solid #dadada;border-radius: 7px;}.form-control:focus {outline: none;border-color: #dadada;box-shadow: 0 0 14px #4a4a4a;}.step:last-child {border-bottom: none;padding-bottom: 0rem;}.wrapper-dropdown {position: relative;margin: 0 auto;margin-bottom: 10px;padding: 12px 15px;background: #fff;border-radius: 5px;cursor: pointer;outline: none;transition: all 0.3s ease-out;}.wrapper-dropdown:after {content: "";width: 0;height: 0;position: absolute;top: 50%;right: 15px;margin-top: -3px;border-width: 6px 6px 0 6px;border-style: solid;border-color: #38d47f transparent;}.success-outline.wrapper-dropdown:after {border-color: green transparent;}.wrapper-dropdown .dropdown {position: absolute;top: 100%;left: 0;right: 0;background: #fff;border-radius: 0 0 5px 5px;border: 1px solid rgba(0,0,0,0.2);border-top: none;border-bottom: none;list-style: none;transition: all 0.3s ease-out;padding-left: 0px;max-height: 0;overflow: hidden;z-index: 999;}.wrapper-dropdown .dropdown li {}.wrapper-dropdown .dropdown li a {display: block;text-decoration: none;color: #333;padding: 10px 0;transition: all 0.3s ease-out;border-bottom: 1px solid #e6e8ea;}.wrapper-dropdown .dropdown li a:hover {color: #38d47f !important;}.wrapper-dropdown .dropdown li:last-of-type a {border: none;}.wrapper-dropdown .dropdown li i {margin-right: 5px;color: inherit;vertical-align: middle;}.wrapper-dropdown .dropdown li:hover a {color: #57a9d9;background-color: #eee;}.wrapper-dropdown.active {border-radius: 5px 5px 0 0;background: #38d47f;box-shadow: none;border-bottom: 2px solid #eceeef;color: white;border: none;}.wrapper-dropdown.active:after {border-color: #FFF transparent;}.wrapper-dropdown.active .dropdown {border-bottom: 1px solid rgba(0,0,0,0.2);max-height: 400px;overflow: scroll;}input#search-token {width: 100%;height: 45px;border: none;border-bottom: 1px solid #eceeef;text-align: center;color: #888888;}input#search-token:focus, input#search-token:focus-visible {outline: #eceeef;}input#search-token::placeholder {color: #c7c7c7;}.explanation {font-size: smaller;color: #999;}.fileupload-wrapper {display: block;position: relative;cursor: pointer;overflow: hidden;width: 100%;max-width: 100%;padding: 5px 10px;margin-top:15px;font-size: 14px;line-height: 22px;color: #777;background-color: #FFF;background-image: none;text-align: center;border: 2px solid #E5E5E5;-webkit-transition: border-color .15s linear;transition: border-color .15s linear;}.fileupload-wrapper .fileupload-message {position: relative;-webkit-transform: translateY(35%);transform: translateY(35%);text-align: center;font-family: sans-serif;font-size: 16px;color: #bbb;}.fileupload-wrapper input {position: absolute;top: 0;right: 0;bottom: 0;left: 0;height: 100%;width: 100%;opacity: 0;cursor: pointer;z-index: 5;}.fileupload-wrapper .fileupload-filename {font-size: 16px;position: relative;transform: translateY(35%);}.fileupload-wrapper p {padding-bottom: 0px;}#create-token-p{margin-top: 1rem;margin-bottom: 0rem;}#save.btn-fullwidth {width: 100%;background-color: #38d47f;}a.btn-success.btn {background-color: #38d47f;width: 100%;border-color: #38d47f;color: white;}button.btn.btn-success:disabled {opacity: .85;}@media (min-width: 500px) {#save.btn-fullwidth {font-size: 2rem;}}#save.btn-disabled {font-size: 1rem;width: 14rem;background-color: #ec6161;border: none;}a.btn-success.btn:hover, a.btn-success.btn:focus {color: #fff;}.result {display: none;}#result_cloned_website {height: 10rem;}.jumbotron .btn.btn-clipboard {background-color: #9d8;padding: 5px .5rem;font-size: inherit;cursor: pointer;margin-left: -5px;border-top-left-radius: 0;border-bottom-left-radius: 0;}.btn-clipboard > img {max-width: 15px;height: 15px;}.result-data {width: 80%;border: none;font-family: monospace;padding: 5px 12px;font-size: 17px;line-height: 20px;color: #24292e;vertical-align: middle;background-color: #ffffff;background-repeat: no-repeat;background-position: right 8px center;border: 1px solid #e1e4e8;border-radius: 6px;outline: none;box-shadow: rgba(225, 228, 232, 0.2) 0px 1px 0px 0px inset;border-top-right-radius: 0;border-bottom-right-radius: 0;}.result-data:focus{outline: none;box-shadow: #01ac4b 0px 0px 0px 1px;}.credit-card {display: flex;justify-content: space-evenly;margin-bottom: 15px;}.copy-card .card-detail {display: flex;align-items: flex-end;}.copy-card .card-detail div {display: flex;flex-direction: column;margin-left: 10px;align-items: flex-start;}.copy-card .card-detail .title {font-size: 11.5px;margin-bottom: 5px;font-weight: 300;}.copy-card {display: flex;flex-wrap: wrap;flex-direction: column;justify-content: space-around;font-size: 15px;margin: 15px 0 20px 0;}.copy-card #copy-name, .copy-card #copy-card, .copy-card #copy-expiry, .copy-card #copy-cvc {font-family: monospace;color: #646464;font-weight: 600;font-size: 16px;}.copy-card img {width: 17px;height: 17px;margin-bottom: 3px;}.copy-card img:hover {cursor: pointer;border-radius: 50%;border: 1px solid #24b47e;padding: 2px;}.copy-card .btn-clipboard.hide-button {background: white;border: none;outline: none;height: 22px;width: 22px;background-image: url(/resources/copy-inactive.svg);background-size: 22px;background-repeat: no-repeat;background-position: center;}.copy-card .btn-clipboard.hide-button:hover {background-image: url(/resources/copy-hover.svg);cursor: pointer;}#ccRender {margin-top: 15px;margin-bottom: 20px;width: 335px;}#cccontainer {font-family: 'OCR A Extended', sans-serif;position: relative;margin: auto;background-image: url(/resources/cc-background-AMEX.png);height: 205px;background-repeat: no-repeat;background-size: contain;color: white;}#cccontainer #ccname {left: 30px;top: 75px;position: absolute;font-size: 16pt;}#cccontainer #ccnumber {left: 30px;top: 100px;position: absolute;font-size: 14pt;word-spacing: 0.45em;}#cccontainer #ccexpires {left: 25px;top: 160px;position: absolute;font-size: 13pt;}#cccontainer #cccvc {left: 165px;top: 160px;position: absolute;font-size: 13pt;}@media screen and (max-width: 768px) {.credit-card {flex-wrap: wrap;margin-bottom: 0;}.copy-card {display: grid;grid-template-columns: auto auto;column-gap: 20px;row-gap: 20px;}}@media screen and (max-width: 600px) {.copy-card {column-gap: 10px;row-gap: 10px;margin: 10px auto;}.copy-card span {margin-bottom: 3px;display: flex;}.copy-card .card-detail .title {font-size: 11px;text-align: left;}.copy-card #copy-name, .copy-card #copy-card, .copy-card #copy-expiry, .copy-card #copy-cvc {font-size: 11px;text-align: left;}#cccontainer {height: 190px !important;width: auto !important;background-repeat: no-repeat;background-size: contain;}#cccontainer #ccname {left: 25px !important;top: 70px !important;font-size: 15pt !important;}#cccontainer #ccnumber {left: 25px !important;top: 95px !important;font-size: 13pt !important;}#cccontainer #ccexpires {left: 25px !important;top: 145px !important;font-size: 12pt !important;}#cccontainer #cccvc {left: 145px !important;top: 145px !important;font-size: 12pt !important;}}@media screen and (min-width: 375px) and (max-width: 425px) {#cccontainer {height: 160px !important;width: 240px !important;}#cccontainer #ccname {left: 20px !important;top: 60px !important;font-size: 13pt !important;}#cccontainer #ccnumber {left: 20px !important;top: 80px !important;font-size: 10pt !important;}#cccontainer #ccexpires {left: 20px !important;top: 119px !important;font-size: 9pt !important;}#cccontainer #cccvc {left: 120px !important;top: 119px !important;font-size: 9pt !important;}}@media screen and (min-width: 320px) and (max-width: 375px) {.copy-card .card-detail .title {font-size: 9.5px;text-align: left;}.copy-card #copy-name, .copy-card #copy-card, .copy-card #copy-expiry, .copy-card #copy-cvc {font-size: 10px;text-align: left;}.copy-card img {width: 15px;height: 15px;}#cccontainer {height: 130px !important;width: 190px !important;}#cccontainer #ccname {left: 15px !important;top: 45px !important;font-size: 10pt !important;}#cccontainer #ccnumber {left: 15px !important;top: 60px !important;font-size: 9pt !important;}#cccontainer #ccexpires {left: 15px !important;top: 92px !important;font-size: 9pt !important;}#cccontainer #cccvc {left: 95px !important;top: 92px !important;font-size: 9pt !important;}}.advice {text-align: left;margin-top: 2rem;}.advice li {list-style: none;position: relative;margin: 20px 0 20px 0;font-size: 15px;}.advice li::before {content: '';background-image: url('/resources/idea.svg');background-size: contain;background-repeat: no-repeat;background-position: center;background-size: 2em;height: 100%;width: 30px;position: absolute;left: -40px;}@media screen and (max-width: 600px) {.advice li {margin: 10px 0 10px 0;font-size: 14px;}}.create-link {text-align: center;margin-bottom: 15px;}.create-link a {}a.refresh {font-size: 1.5rem;vertical-align: middle;text-decoration: none;margin-right: 5px;}a.refresh:hover{cursor: pointer;}.pre-like {display: block;font-family: monospace;white-space: pre;margin: 1em 0px;word-break: break-all;overflow: scroll;text-align: left;}.error-field {display: none;color: red;border-bottom: 1px solid red;border-radius: 6px;margin-bottom: 24px;}#endpoints_errors {border: 1px solid red;margin: 24px 0px;padding: 0px 5px;}#endpoints_errors pre {color: #eceeef;background: firebrick;}.map{position: relative;margin-bottom: 10px;margin-top: 10px;height:400px;border-radius: 5px;border: 7px solid #dadada;}.setting .key {text-align: right;}.setting .value {text-align: left;}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary:hover, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary:hover {background-color: #449d44;border-color: #419641;}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary, .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary {color: #fff;background-color: #38d47f;border-color: #38d47f;}.jumbotron .btn.btn-clipboard {background-color: #38d47f;border-color: #38d47f;color: #fff;}.setting .key small {font-size: .6rem;color: #999;vertical-align: top;}@media (min-width: 48em) {.container {max-width: 100%;}}.jumbotron {max-width: 46rem;margin: auto;margin-bottom: 2rem;}a.icon:before {display: block;position: absolute;margin-right: 10px;margin-left: 10px;content: "";height: 40px;width: 40px;background-size: contain;-webkit-transition: all 0.4s ease-in-out;-moz-transition: all 0.4s ease-in-out;transition: all 0.4s ease-in-out;-webkit-transform: translate3d(0, 0, 0);}@media (max-width: 768px) {a.icon:before {width: 20px;height: 20px;}}a.icon-web:before {background-image: url("/resources/web.png");}a.icon-dns:before {background-image: url("/resources/dns.png");}a.icon-web-image:before {background-image: url("/resources/web_image.png");}a.icon-email:before {background-image: url("/resources/email.png");}a.icon-word:before {background-image: url("/resources/word.png");}a.icon-cc:before {background-image: url("/resources/credit-card.png");}a.icon-excel:before {background-image: url("/resources/excel.png");}a.icon-pdf:before {background-image: url("/resources/pdf.png");}a.icon-wireguard:before {background-image: url("/resources/wireguard.png");}a.icon-folder:before {background-image: url("/resources/folder.png");}a.icon-exe:before {background-image: url("/resources/exe.png");}a.icon-clonedsite:before {background-image: url("/resources/clonedsite.png");}a.icon-sqlserver:before {background-image: url("/resources/sqlserver.png");}a.icon-mysql:before {background-image: url("/resources/mysql.png");}a.icon-qrcode:before {background-image: url("/resources/qrcode.png");}a.icon-svn:before {background-image: url("/resources/svn.png");}a.icon-aws:before {background-image: url("/resources/aws.png");}a.icon-azure-id:before {background-image: url("/resources/azure-id.png");}a.icon-redirect:before {background-image: url("/resources/redirect.png");}a.icon-kubernetes:before {background-image: url("/resources/kubernetes_icon.png");}a.icon-log4shell:before {background-image: url("/resources/log4j.png");}a.icon-msreg:before {background-image: url("/resources/msreg.png");}@media (max-width: 768px) {input.form-control:-moz-placeholder, textarea.form-control:-moz-placeholder {font-size: .8rem;}input.form-control::-moz-placeholder, textarea.form-control::-moz-placeholder {font-size: .8rem;}input.form-control:placeholder, textarea.form-control:placeholder {font-size: .8rem;}input.form-control::-webkit-input-placeholder, textarea.form-control::-webkit-input-placeholder {font-size: .8rem;}.setting .key {text-align: center;}.setting .value {text-align: center;margin-bottom: 1rem;}.artifacts{margin-left: 10px;margin-right: 10px;}.row.results{margin-left: 10px;margin-right: 10px;}}@media (max-width: 500px) {input.form-control:-moz-placeholder, textarea.form-control:-moz-placeholder{font-size: .5rem;}input.form-control::-moz-placeholder, textarea.form-control::-moz-placeholder {font-size: .5rem;}input.form-control:placeholder, textarea.form-control:placeholder {font-size: .5rem;}input.form-control::-webkit-input-placeholder, textarea.form-control::-webkit-input-placeholder {font-size: .5rem;}.token-length{font-size: 1.2rem;}.setting .key {text-align: center;}.setting .value {text-align: center;margin-bottom: 1rem;}.artifacts{margin-left: 10px;margin-right: 10px;}.row.results{margin-left: 5px;margin-right: 5px;}a.btn.btn-large.btn-success{font-size: 0.9rem;white-space: pre-wrap;}}#mainsite {font-size: smaller;padding-top: 1.5rem;}.mysql-encoded-button {margin-top: 10px;text-align: center;}.mysql-text {text-align: left;}.mysqldownload {cursor: pointer;}#wg_conf {background: #f6f6f6;display: block;white-space: pre;overflow-x: scroll;max-width: 100%;min-width: 100px;padding: 0;text-align: left;border-radius: 8px;padding-bottom: 12px;padding-left: 12px;padding-top: 12px;margin-bottom: 7px;margin-top: 7px;margin-left: 0px;}#wireguard-app {margin-bottom: 16px;}#wireguard-app img {height: 40px;}a:focus.what-is-this, a:hover.what-is-this {text-decoration: none;}a.what-is-this svg {width: 19px;} diff --git a/tests/integration/test_against_token_server.py b/tests/integration/test_against_token_server.py index 9f88c8cd8..ae5a33c83 100644 --- a/tests/integration/test_against_token_server.py +++ b/tests/integration/test_against_token_server.py @@ -658,41 +658,3 @@ def test_token_error_codes( resp.raise_for_status() code = resp.json()[error] assert code == error_code - - -# try: -# token_request_details = parse_obj_as(AnyTokenRequest, token_request_data) -# except ValidationError: # DESIGN: can we specialise on what went wrong? -# return response_error(1, "No email/webhook supplied or malformed request") - -# if not token_request_details.memo: -# return response_error(2, "No memo supplied") - -# if token_request_details.webhook_url: -# try: -# validate_webhook( -# token_request_details.webhook_url, token_request_details.token_type -# ) -# except requests.exceptions.HTTPError: -# # raise HTTPException(status_code=400, detail="Failed to validate webhook") -# return response_error( -# 3, "Invalid webhook supplied. Confirm you can POST to this URL." -# ) -# except requests.exceptions.ConnectTimeout: -# # raise HTTPException( -# # status_code=400, detail="Failed to validate webhook - timed out." -# # ) -# return response_error( -# 3, "Webhook timed out. Confirm you can POST to this URL." -# ) - -# if token_request_details.email: -# if not is_valid_email(token_request_details.email): -# return response_error(5, "Invalid email supplied") - -# if is_email_blocked(token_request_details.email): -# # raise HTTPException(status_code=400, detail="Email is blocked.") -# return response_error( -# 6, -# "Blocked email supplied. Please see our Acceptable Use Policy at https://canarytokens.org/legal", -# ) diff --git a/tests/units/test_extendtoken.py b/tests/units/test_extendtoken.py new file mode 100644 index 000000000..c53905a33 --- /dev/null +++ b/tests/units/test_extendtoken.py @@ -0,0 +1,24 @@ +import os +from distutils.util import strtobool + +import pytest + +from canarytokens import extendtoken +from canarytokens.settings import FrontendSettings + +settings = FrontendSettings("../frontend/frontend.env") + + +@pytest.mark.skipif( + not strtobool(os.getenv("MAKE_CARD", "False")), + reason="We don't want to use up a cc every time we run tests", +) +def test_create_cc(): + eapi = extendtoken.ExtendAPI( + email=settings.EXTEND_EMAIL, + password=settings.EXTEND_PASSWORD.get_secret_value(), + card_name=settings.EXTEND_CARD_NAME, + ) + token_url = "http://canarytokens.com/static/tags/traffic/u27uxknlv3x2wzpe5ufkehpmh/contact.php" + cc = eapi.create_credit_card(token_url=token_url) + assert len(cc.number) == 15 diff --git a/tests/units/test_frontend.py b/tests/units/test_frontend.py index c5a6dd38d..cf0828f90 100644 --- a/tests/units/test_frontend.py +++ b/tests/units/test_frontend.py @@ -15,6 +15,8 @@ AWSKeyTokenRequest, AWSKeyTokenResponse, BrowserScannerSettingsRequest, + CCTokenRequest, + CCTokenResponse, CustomBinaryTokenRequest, CustomBinaryTokenResponse, CustomImageTokenRequest, @@ -129,10 +131,12 @@ def test_generate_log4shell_token(test_client: TestClient) -> None: # TODO: test client uploads is not added. # Skipping these types for now. set_of_unsupported_request_classes = [ + CCTokenRequest, CustomImageTokenRequest, CustomBinaryTokenRequest, ] set_of_unsupported_response_classes = [ + CCTokenResponse, CustomImageTokenResponse, CustomBinaryTokenResponse, ] @@ -247,6 +251,7 @@ def test_download_canarydrop_csv_details( (PDFTokenRequest, PDFTokenResponse, DownloadPDFRequest), (MySQLTokenRequest, MySQLTokenResponse, DownloadMySQLRequest), # (AWSKeyTokenRequest, AWSKeyTokenResponse, DownloadAWSKeysRequest), + # (CCTokenRequest, CCTokenResponse, DownloadCCRequest), (KubeconfigTokenRequest, KubeconfigTokenResponse, DownloadKubeconfigRequest), (QRCodeTokenRequest, QRCodeTokenResponse, DownloadQRCodeRequest), ], diff --git a/tests/units/test_http_channel.py b/tests/units/test_http_channel.py index df686f234..2593666ef 100644 --- a/tests/units/test_http_channel.py +++ b/tests/units/test_http_channel.py @@ -4,13 +4,19 @@ from pydantic import EmailStr from twisted.internet.address import IPv4Address from twisted.web.http import Request +from twisted.web.http_headers import Headers from twisted.web.test.requesthelper import DummyChannel from twisted.web.test.test_web import DummyRequest from canarytokens import canarydrop, queries from canarytokens.awskeys import get_aws_key from canarytokens.channel_http import ChannelHTTP -from canarytokens.models import AWSKeyTokenHistory, TokenTypes +from canarytokens.models import ( + AWSKeyTokenHistory, + CCTokenHistory, + CreditCard, + TokenTypes, +) from canarytokens.settings import FrontendSettings, Settings from canarytokens.switchboard import Switchboard from canarytokens.tokens import Canarytoken @@ -53,7 +59,8 @@ def test_channel_http_GET(setup_db, settings, frontend_settings, token_type): client = IPv4Address(type="TCP", host="127.0.0.1", port=8686) request = DummyRequest("/") request.client = client - request.path = cd.generate_random_url(["http://127.0.0.1:8686"]) + request.uri = cd.generate_random_url(["http://127.0.0.1:8686"]).encode() + request.path = request.uri[request.uri.index(b"/", 8) :] # noqa: E203 http_channel.canarytoken_page.render_GET(request) cd_updated = queries.get_canarydrop(canarytoken=cd.canarytoken) assert len(cd_updated.triggered_details.hits) == 1 @@ -104,7 +111,8 @@ def test_channel_http_GET_and_POST_back( queries.save_canarydrop(cd) request = Request(channel=DummyChannel()) - request.path = cd.generate_random_url(["http://127.0.0.1"]) + request.uri = cd.generate_random_url(["http://127.0.0.1:8686"]).encode() + request.path = request.uri[request.uri.index(b"/", 8) :] # noqa: E203 request.args = request_args request.method = b"GET" http_channel.site.resource.render(request) @@ -177,7 +185,7 @@ def test_channel_http_GET_random_endpoint(setup_db, settings, frontend_settings) # TODO: Add random endpoints. request.path = "//".join( cd.generate_random_url(["http://127.0.0.1"]).split("/")[::2][:2] - ) + ).encode() request.args = request_args request.method = b"GET" resp = http_channel.site.resource.render(request) @@ -190,7 +198,7 @@ def test_channel_http_GET_random_endpoint(setup_db, settings, frontend_settings) request = Request(channel=DummyChannel()) # TODO: Add random endpoints. - request.path = "http://127.0.0.1/this/has/no-token" + request.path = b"http://127.0.0.1/this/has/no-token" request.args = request_args request.method = b"POST" @@ -275,3 +283,90 @@ def test_POST_aws_token_back( "eventName" ] == ["GetCallerIdentity"] assert cd.type == cd_updated.type + + +def test_GET_cc_token_back( + frontend_settings: FrontendSettings, + settings: Settings, + setup_db: None, +): + http_channel = ChannelHTTP( + settings=settings, + frontend_settings=frontend_settings, + switchboard=switchboard, + ) + + canarytoken = Canarytoken() + cc = CreditCard( + id="vc_1234", + number="370012340001234", + name="Bob Smith", + billing_zip=10210, + address="some billing address", + ) + + cd = canarydrop.Canarydrop( + type=TokenTypes.CC, + triggered_details=CCTokenHistory(), + alert_email_enabled=False, + alert_email_recipient=EmailStr("email@test.com"), + alert_webhook_enabled=False, + alert_webhook_url=None, + canarytoken=canarytoken, + memo="memo", + cc_id=cc.id, + cc_kind=cc.kind, + cc_number=cc.number, + cc_cvc=cc.cvc, + cc_expiration=cc.expiration, + cc_name=cc.name, + cc_billing_zip=cc.billing_zip, + cc_address=cc.address, + cc_rendered_html=cc.render_html(), + ) + queries.save_canarydrop(cd) + + # stripped down version of the data we get from Extend + resp = { + "transaction": { + "virtualCardId": cc.id, + "vcnLast4": cc.number[-4:], + "authMerchantAmountCents": "639", + "merchantName": "ACME Airline Co.", + } + } + # mimic the processing done on the above + txn = resp.get("transaction") + last4 = txn.get("vcnLast4", "") + amount = "{:.2f}".format(int(txn.get("authMerchantAmountCents", "0")) / 100) + merchant = txn.get("merchantName", "") + + vc_info = { + "virtualCard": { + "id": cc.id, + "last4": cc.number[-4:], + "notes": cd.get_url( + [f"{settings.DOMAINS[0]}:{settings.CHANNEL_HTTP_PORT}"] + ), + } + } + + request = Request(channel=DummyChannel()) + request.args = {} + request.uri = vc_info["virtualCard"].get("notes").encode() + request.path = request.uri[request.uri.index(b"/", 8) :] # noqa: E203 + headers = { + "Type": ["Credit Card"], + "Last4": [last4], + "Amount": [amount], + "Merchant": [merchant], + } + request.requestHeaders = Headers(headers) + request.method = b"GET" + http_channel.site.resource.render(request) + + cd_updated = queries.get_canarydrop(canarytoken=cd.canarytoken) + + assert cd_updated is not None + assert len(cd_updated.triggered_details.hits) == 1 + assert cd.type == cd_updated.type