From 91f69d25be6f06fbf8ab62bc20af029ed1b10c59 Mon Sep 17 00:00:00 2001
From: William Leighton Dawson
Date: Thu, 6 Apr 2023 21:26:55 +0200
Subject: [PATCH 1/5] Add azure id token
---
.github/workflows/test.yml | 2 +-
canarytokens/awskeys.py | 11 +-
canarytokens/azurekeys.py | 50 ++++++++
canarytokens/canarydrop.py | 8 ++
canarytokens/channel_http.py | 5 +
canarytokens/models.py | 143 ++++++++++++++++++++++
canarytokens/settings.py | 3 +
canarytokens/tokens.py | 67 +++++++++-
frontend/app.py | 103 +++++++++++++++-
switchboard/switchboard.env.dist | 2 +
templates/generate_new.html | 63 +++++++++-
templates/history.html | 4 +
templates/manage_new.html | 44 +++----
tests/integration/test_azure_key_token.py | 113 +++++++++++++++++
tests/utils.py | 23 ++++
15 files changed, 605 insertions(+), 36 deletions(-)
create mode 100644 canarytokens/azurekeys.py
create mode 100644 tests/integration/test_azure_key_token.py
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 76190a11b..ea438f555 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -4,7 +4,7 @@ on:
push:
branches:
- "T4627_py3_main"
- - "T5281_port_cc_token"
+ - "T5285_port_azure_token"
jobs:
tests:
diff --git a/canarytokens/awskeys.py b/canarytokens/awskeys.py
index 54883bae0..7194c3a13 100644
--- a/canarytokens/awskeys.py
+++ b/canarytokens/awskeys.py
@@ -1,19 +1,12 @@
import logging
import re
-from typing import Literal, Optional, TypedDict
+from typing import Optional
import requests
from pydantic import HttpUrl
from canarytokens import tokens
-
-
-class AWSKey(TypedDict):
- access_key_id: str
- secret_access_key: str
- # TODO: make enum
- region: str
- output: Literal["json", "yaml", "yaml-stream", "text", "table"]
+from canarytokens.models import AWSKey
def validate_record(server: str, token: tokens.Canarytoken) -> bool:
diff --git a/canarytokens/azurekeys.py b/canarytokens/azurekeys.py
new file mode 100644
index 000000000..7e398bf4d
--- /dev/null
+++ b/canarytokens/azurekeys.py
@@ -0,0 +1,50 @@
+import logging
+from typing import Optional
+
+import requests
+from pydantic import HttpUrl
+
+from canarytokens.models import AzureID
+from canarytokens import tokens
+
+
+def get_azure_id(
+ token: tokens.Canarytoken,
+ server: str,
+ cert_file_name: str,
+ azure_url: Optional[HttpUrl] = None,
+ app_id: Optional[str] = None,
+ cert: Optional[str] = None,
+ tenant_id: Optional[str] = None,
+ cert_name: Optional[str] = None,
+) -> AzureID:
+ if app_id and cert and tenant_id and cert_name:
+ return AzureID(
+ **{
+ "app_id": app_id,
+ "cert": cert,
+ "tenant_id": tenant_id,
+ "cert_name": cert_name,
+ "cert_file_name": cert_file_name,
+ }
+ )
+ if not (token and server) or len(server) == 0:
+ logging.error("Empty values passed through to get_azure_id function.")
+ raise ValueError("get_azure_id requires token and server to be set.")
+ if not azure_url:
+ raise ValueError("get_azure_id requires azure_url to request from.")
+
+ data = {"token": token.value(), "domain": server}
+
+ resp = requests.post(url=azure_url, json=data)
+ resp.raise_for_status()
+ resp_json = resp.json()
+ return AzureID(
+ **{
+ "app_id": resp_json["app_id"],
+ "cert": resp_json["cert"],
+ "tenant_id": resp_json["tenant_id"],
+ "cert_name": resp_json["cert_name"],
+ "cert_file_name": cert_file_name,
+ }
+ )
diff --git a/canarytokens/canarydrop.py b/canarytokens/canarydrop.py
index 117e232cf..6a0f43f65 100644
--- a/canarytokens/canarydrop.py
+++ b/canarytokens/canarydrop.py
@@ -112,6 +112,14 @@ class Canarydrop(BaseModel):
aws_secret_access_key: Optional[str]
aws_output: Optional[str] = Field(alias="output")
aws_region: Optional[str] = Field(alias="region")
+
+ # Azure key specific stuff
+ app_id: Optional[str]
+ tenant_id: Optional[str]
+ cert: Optional[str]
+ cert_name: Optional[str]
+ cert_file_name: Optional[str]
+
# HTTP style token specific stuff
browser_scanner_enabled: Optional[bool]
# Wireguard specific stuff
diff --git a/canarytokens/channel_http.py b/canarytokens/channel_http.py
index 3c0700fa5..2b78b1268 100644
--- a/canarytokens/channel_http.py
+++ b/canarytokens/channel_http.py
@@ -127,6 +127,11 @@ def render_POST(self, request: Request):
canarydrop.add_canarydrop_hit(token_hit=token_hit)
self.dispatch(canarydrop=canarydrop, token_hit=token_hit)
return b"success"
+ elif canarydrop.type == TokenTypes.AZURE_KEYS:
+ token_hit = Canarytoken._parse_azure_key_trigger(request)
+ canarydrop.add_canarydrop_hit(token_hit=token_hit)
+ self.dispatch(canarydrop=canarydrop, token_hit=token_hit)
+ return b"success"
elif canarydrop.type in [
TokenTypes.SLOW_REDIRECT,
TokenTypes.WEB_IMAGE,
diff --git a/canarytokens/models.py b/canarytokens/models.py
index 19ae5e923..f8fda3f77 100644
--- a/canarytokens/models.py
+++ b/canarytokens/models.py
@@ -159,6 +159,22 @@ def live(self) -> bool:
return strtobool(os.getenv("LIVE", "FALSE"))
+class AWSKey(TypedDict):
+ access_key_id: str
+ secret_access_key: str
+ # TODO: make enum
+ region: str
+ output: Literal["json", "yaml", "yaml-stream", "text", "table"]
+
+
+class AzureID(TypedDict):
+ app_id: str
+ tenant_id: str
+ cert: str
+ cert_name: str
+ cert_file_name: str
+
+
class KubeCerts(TypedDict):
"""Kube digest (f), cert (c) and key (k) are stored directly and not
base64 encoded.
@@ -280,6 +296,7 @@ class TokenTypes(str, enum.Enum):
SQL_SERVER = "sql_server"
MY_SQL = "my_sql"
AWS_KEYS = "aws_keys"
+ AZURE_KEYS = "azure_id"
SIGNED_EXE = "signed_exe"
FAST_REDIRECT = "fast_redirect"
SLOW_REDIRECT = "slow_redirect"
@@ -411,6 +428,22 @@ class AWSKeyTokenRequest(TokenRequest):
token_type: Literal[TokenTypes.AWS_KEYS] = TokenTypes.AWS_KEYS
+class AzureIDTokenRequest(TokenRequest):
+ token_type: Literal[TokenTypes.AZURE_KEYS] = TokenTypes.AZURE_KEYS
+ azure_id_cert_file_name: str
+
+ def v2_dict(self) -> Dict[str, Any]:
+ webhook_url = self.webhook_url or ""
+ out_dict = {
+ "type": getattr(self, "token_type").value,
+ "email": self.email or "",
+ "memo": self.memo,
+ "webhook": str(webhook_url),
+ "azure_id_cert_file_name": self.azure_id_cert_file_name,
+ }
+ return out_dict
+
+
class QRCodeTokenRequest(TokenRequest):
token_type: Literal[TokenTypes.QR_CODE] = TokenTypes.QR_CODE
@@ -598,6 +631,7 @@ class WindowsDirectoryTokenRequest(TokenRequest):
FastRedirectTokenRequest,
QRCodeTokenRequest,
AWSKeyTokenRequest,
+ AzureIDTokenRequest,
PDFTokenRequest,
DNSTokenRequest,
Log4ShellTokenRequest,
@@ -670,6 +704,15 @@ class AWSKeyTokenResponse(TokenResponse):
output: str
+class AzureIDTokenResponse(TokenResponse):
+ token_type: Literal[TokenTypes.AZURE_KEYS] = TokenTypes.AZURE_KEYS
+ app_id: str
+ tenant_id: str
+ cert: str
+ cert_name: str
+ cert_file_name: str
+
+
class PDFTokenResponse(TokenResponse):
token_type: Literal[TokenTypes.ADOBE_PDF] = TokenTypes.ADOBE_PDF
hostname: str # Hostname Local testing fails this check TODO: FIXME
@@ -880,6 +923,7 @@ class MySQLTokenResponse(TokenResponse):
CustomBinaryTokenResponse,
SlowRedirectTokenResponse,
AWSKeyTokenResponse,
+ AzureIDTokenResponse,
MsWordDocumentTokenResponse,
Log4ShellTokenResponse,
MsExcelDocumentTokenResponse,
@@ -1045,6 +1089,47 @@ def normalize_additional_info_names(cls, values: dict[str, Any]) -> dict[str, An
return {k.lower(): v for k, v in values.items()}
+class AzureKeyAdditionalInfo(BaseModel):
+ azure_key_log_data: dict[str, list[str]]
+ microsoft_azure: dict[str, list[str]]
+ location: dict[str, list[str]]
+ coordinates: dict[str, list[str]]
+
+ @root_validator(pre=True)
+ def normalize_additional_info_names(cls, values: dict[str, Any]) -> dict[str, Any]: # type: ignore
+ keys_to_convert = [
+ # TODO: make this consistent.
+ ("Azure ID Log Data", "azure_key_log_data"),
+ ("Microsoft Azure", "microsoft_azure"),
+ ("Location", "location"),
+ ("Coordinates", "coordinates"),
+ ]
+ for old_key, new_key in keys_to_convert: # pragma: no cover
+ if old_key in values:
+ values[new_key] = values.pop(old_key)
+
+ return {k.lower(): v for k, v in values.items()}
+
+ def serialize_for_v2(self) -> dict:
+ """Serialize an `AzureIDTokenHit` into a dict
+ that holds the equivalent info in the v2 shape.
+ Returns:
+ dict: AzureIDTokenHit in v2 dict representation.
+ """
+ data = self.dict()
+ keys_to_convert = [
+ # TODO: make this consistent.
+ ("Azure ID Log Data", "azure_key_log_data"),
+ ("Microsoft Azure", "microsoft_azure"),
+ ("Location", "location"),
+ ("Coordinates", "coordinates"),
+ ]
+ for value, key in keys_to_convert:
+ if key in data:
+ data[value] = data.pop(key)
+ return data
+
+
class AdditionalInfo(BaseModel):
# the ServiceInfo keys are dynamic
# this only works for our test
@@ -1141,6 +1226,25 @@ def adjust_geo_info(cls, value):
return value
+class AzureIDTokenHit(TokenHit):
+ token_type: Literal[TokenTypes.AZURE_KEYS] = TokenTypes.AZURE_KEYS
+ additional_info: Optional[AzureKeyAdditionalInfo]
+
+ class Config:
+ allow_population_by_field_name = True
+
+ def serialize_for_v2(self) -> dict:
+ """Serialize an `AzureIDTokenHit` into a dict
+ that holds the equivalent info in the v2 shape.
+ Returns:
+ dict: AzureIDTokenHit in v2 dict representation.
+ """
+ data = json_safe_dict(self, exclude=("token_type", "time_of_hit"))
+ if "additional_info" in data:
+ data["additional_info"] = self.additional_info.serialize_for_v2()
+ return data
+
+
class AWSKeyTokenHit(TokenHit):
token_type: Literal[TokenTypes.AWS_KEYS] = TokenTypes.AWS_KEYS
useragent: Optional[str] = Field(
@@ -1339,6 +1443,7 @@ class WireguardTokenHit(TokenHit):
CMDTokenHit,
DNSTokenHit,
AWSKeyTokenHit,
+ AzureIDTokenHit,
PDFTokenHit,
ClonedWebTokenHit,
Log4ShellTokenHit,
@@ -1443,6 +1548,11 @@ class AWSKeyTokenHistory(TokenHistory[AWSKeyTokenHit]):
hits: List[AWSKeyTokenHit]
+class AzureIDTokenHistory(TokenHistory):
+ token_type: Literal[TokenTypes.AZURE_KEYS] = TokenTypes.AZURE_KEYS
+ hits: List[AzureIDTokenHit]
+
+
class DNSTokenHistory(TokenHistory[DNSTokenHit]):
token_type: Literal[TokenTypes.DNS] = TokenTypes.DNS
hits: List[DNSTokenHit]
@@ -1600,6 +1710,7 @@ class SvnTokenHistory(TokenHistory[SvnTokenHit]):
CMDTokenHistory,
DNSTokenHistory,
AWSKeyTokenHistory,
+ AzureIDTokenHistory,
PDFTokenHistory,
SMTPTokenHistory,
ClonedWebTokenHistory,
@@ -1813,6 +1924,8 @@ class DownloadFmtTypes(str, enum.Enum):
MSEXCEL = "msexcel"
PDF = "pdf"
AWSKEYS = "awskeys"
+ AZUREIDCONFIG = "azure_id_config"
+ AZUREIDCERT = "azure_id"
KUBECONFIG = "kubeconfig"
SLACKAPI = "slackapi"
INCIDENTLISTJSON = "incidentlist_json"
@@ -1887,6 +2000,14 @@ class DownloadAWSKeysRequest(TokenDownloadRequest):
fmt: Literal[DownloadFmtTypes.AWSKEYS] = DownloadFmtTypes.AWSKEYS
+class DownloadAzureIDConfigRequest(TokenDownloadRequest):
+ fmt: Literal[DownloadFmtTypes.AZUREIDCONFIG] = DownloadFmtTypes.AZUREIDCONFIG
+
+
+class DownloadAzureIDCertRequest(TokenDownloadRequest):
+ fmt: Literal[DownloadFmtTypes.AZUREIDCERT] = DownloadFmtTypes.AZUREIDCERT
+
+
class DownloadCMDRequest(TokenDownloadRequest):
fmt: Literal[DownloadFmtTypes.CMD] = DownloadFmtTypes.CMD
@@ -1906,6 +2027,8 @@ class DownloadSplackApiRequest(TokenDownloadRequest):
AnyDownloadRequest = Annotated[
Union[
DownloadAWSKeysRequest,
+ DownloadAzureIDConfigRequest,
+ DownloadAzureIDCertRequest,
DownloadCCRequest,
DownloadCMDRequest,
DownloadIncidentListCSVRequest,
@@ -2035,6 +2158,26 @@ class DownloadAWSKeysResponse(TokenDownloadResponse):
output: str
+class DownloadAzureIDConfigResponse(TokenDownloadResponse):
+ contenttype: Literal[
+ DownloadContentTypes.TEXTPLAIN
+ ] = DownloadContentTypes.TEXTPLAIN
+ filename: str
+ token: str
+ auth: str
+ ...
+
+
+class DownloadAzureIDCertResponse(TokenDownloadResponse):
+ contenttype: Literal[
+ DownloadContentTypes.TEXTPLAIN
+ ] = DownloadContentTypes.TEXTPLAIN
+ filename: str
+ token: str
+ auth: str
+ ...
+
+
class DownloadKubeconfigResponse(TokenDownloadResponse):
contenttype: Literal[
DownloadContentTypes.TEXTPLAIN
diff --git a/canarytokens/settings.py b/canarytokens/settings.py
index e77f6b4ce..5aa23c1c3 100644
--- a/canarytokens/settings.py
+++ b/canarytokens/settings.py
@@ -25,6 +25,9 @@ class Settings(BaseSettings):
TESTING_AWS_REGION: Optional[str] = "us-east-2"
TESTING_AWS_OUTPUT: Optional[str] = "json"
+ AZURE_ID_TOKEN_URL: HttpUrl
+ AZURE_ID_TOKEN_AUTH: str
+
WG_PRIVATE_KEY_SEED: str
WG_PRIVATE_KEY_N: str = "1000"
diff --git a/canarytokens/tokens.py b/canarytokens/tokens.py
index 2f267e797..31443f742 100644
--- a/canarytokens/tokens.py
+++ b/canarytokens/tokens.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import base64
+import json
import random
import re
from datetime import datetime
@@ -19,7 +20,7 @@
INPUT_CHANNEL_HTTP,
)
from canarytokens.exceptions import NoCanarytokenFound
-from canarytokens.models import AnyTokenHit, AWSKeyTokenHit, TokenTypes
+from canarytokens.models import AnyTokenHit, AWSKeyTokenHit, AzureIDTokenHit, TokenTypes
# TODO: put these in a nicer place. Ensure re.compile is called only once at startup
# add a naming convention for easy reading when seen in other files.
@@ -402,6 +403,70 @@ def _parse_aws_key_trigger(
}
return AWSKeyTokenHit(**hit_info)
+ @staticmethod
+ def _parse_azure_key_trigger(
+ request: Request,
+ ) -> AzureIDTokenHit:
+ """When an AzureKey token is triggered a lambda makes a POST request
+ back to switchboard. The `request` is processed, fields extracted,
+ and an `AzureIDTokenHit` is created.
+
+ Args:
+ request (twisted.web.http.Request): containing Azure Key hit information.
+
+ Returns:
+ AzureIDTokenHit: Structured Azure Key Specific hit info.
+ """
+
+ hit_time = datetime.utcnow().strftime("%s.%f")
+
+ json_data = json.loads(request.content.read())
+ src_ip = json_data.get("ip", "127.0.0.1")
+
+ auth_details = json_data.get("auth_details", "")
+ if type(auth_details) == list:
+ out = ""
+ for d in auth_details:
+ out += "\n{}: {}".format(d["key"], d["value"])
+ auth_details = out
+
+ location_details = json_data.get("location", {})
+ geo_details = location_details.get("geoCoordinates", {})
+
+ additional_info = {
+ "Azure ID Log Data": {
+ "Date": [json_data.get("time", "Not Available")],
+ "Authentication": [auth_details],
+ },
+ "Microsoft Azure": {
+ "Resource": [json_data.get("resource", "Not Available")],
+ "App ID": [json_data.get("app_id", "Not Available")],
+ "Cert ID": [json_data.get("cert_id", "Not Available")],
+ },
+ "Location": {
+ "city": [location_details.get("city", "Not Available")],
+ "state": [location_details.get("state", "Not Available")],
+ "countryOrRegion": [
+ location_details.get("countryOrRegion", "Not Available")
+ ],
+ },
+ "Coordinates": {
+ "latitude": [geo_details.get("latitude", "Not Available")],
+ "longitude": [geo_details.get("longitude", "Not Available")],
+ },
+ }
+ hit_info = {
+ "token_type": TokenTypes.AZURE_KEYS,
+ "time_of_hit": hit_time,
+ "input_channel": INPUT_CHANNEL_HTTP,
+ "src_ip": src_ip,
+ # "geo_info": geo_info,
+ # "is_tor_relay": is_tor_relay,
+ # "user_agent": user_agent,
+ "additional_info": additional_info,
+ }
+ return AzureIDTokenHit(**hit_info)
+
@staticmethod
def _get_info_for_clonedsite(request):
http_general_info = Canarytoken._grab_http_general_info(request=request)
diff --git a/frontend/app.py b/frontend/app.py
index 8d45fc113..d549c672e 100644
--- a/frontend/app.py
+++ b/frontend/app.py
@@ -37,6 +37,7 @@
from canarytokens import wireguard as wg
from canarytokens.authenticode import make_canary_authenticode_binary
from canarytokens.awskeys import get_aws_key
+from canarytokens.azurekeys import get_azure_id
from canarytokens.canarydrop import Canarydrop
from canarytokens.exceptions import CanarydropAuthFailure
from canarytokens.models import (
@@ -46,6 +47,8 @@
AnyTokenResponse,
AWSKeyTokenRequest,
AWSKeyTokenResponse,
+ AzureIDTokenRequest,
+ AzureIDTokenResponse,
CCTokenRequest,
CCTokenResponse,
ClonedWebTokenRequest,
@@ -60,6 +63,10 @@
DNSTokenResponse,
DownloadAWSKeysRequest,
DownloadAWSKeysResponse,
+ DownloadAzureIDCertRequest,
+ DownloadAzureIDCertResponse,
+ DownloadAzureIDConfigRequest,
+ DownloadAzureIDConfigResponse,
DownloadCCRequest,
DownloadCCResponse,
DownloadCMDRequest,
@@ -158,7 +165,7 @@
RedisIntegration(),
FastApiIntegration(),
],
- release=canarytokens.utils.get_deployed_commit_sha(),
+ release=get_deployed_commit_sha(),
)
@@ -303,7 +310,7 @@ def generate_page(request: Request) -> HTMLResponse:
)
async def generate(request: Request) -> AnyTokenResponse: # noqa: C901 # gen is large
"""
- Whatt
+ Generate a token and return the appropriate TokenResponse
"""
if request.headers.get("Content-Type", "application/json") == "application/json":
@@ -687,6 +694,42 @@ def _(
)
+@create_download_response.register
+def _(
+ download_request_details: DownloadAzureIDConfigRequest, canarydrop: Canarydrop
+) -> Response:
+ return DownloadAzureIDConfigResponse(
+ token=download_request_details.token,
+ auth=download_request_details.auth,
+ content=textwrap.dedent(
+ f"""
+ {{
+ "appId": "{canarydrop.app_id}",
+ "displayName": "azure-cli-{canarydrop.cert_name}",
+ "fileWithCertAndPrivateKey": "{canarydrop.cert_file_name}",
+ "password": null,
+ "tenant": "{canarydrop.tenant_id}"
+ }}
+ """
+ ).strip(),
+ filename=canarydrop.cert_file_name.replace(".pem", ".json")
+ if canarydrop.cert_file_name.endswith(".pem")
+ else canarydrop.cert_file_name,
+ )
+
+
+@create_download_response.register
+def _(
+ download_request_details: DownloadAzureIDCertRequest, canarydrop: Canarydrop
+) -> Response:
+ return DownloadAzureIDCertResponse(
+ token=download_request_details.token,
+ auth=download_request_details.auth,
+ content=canarydrop.cert,
+ filename=canarydrop.cert_file_name,
+ )
+
+
@create_download_response.register
def _(
download_request_details: DownloadKubeconfigRequest, canarydrop: Canarydrop
@@ -949,6 +992,62 @@ def _create_aws_key_token_response(
)
+@create_response.register
+def _create_azure_key_token_response(
+ token_request_details: AzureIDTokenRequest,
+ canarydrop: Canarydrop,
+ settings: Optional[Settings] = None,
+) -> AzureIDTokenResponse:
+ if settings is None:
+ settings = switchboard_settings
+
+ try:
+ if not settings.AZURE_ID_TOKEN_URL:
+ raise ValueError("No URL provided for AZURE ID creation")
+
+ if not settings.AZURE_ID_TOKEN_AUTH:
+ raise ValueError("No AUTH token provided for AZURE ID creation")
+
+ key = get_azure_id(
+ token=canarydrop.canarytoken,
+ server=get_all_canary_domains()[0],
+ cert_file_name=token_request_details.azure_id_cert_file_name,
+ azure_url=HttpUrl(
+ f"{settings.AZURE_ID_TOKEN_URL}?code={settings.AZURE_ID_TOKEN_AUTH}",
+ scheme=settings.AZURE_ID_TOKEN_URL.scheme,
+ ),
+ )
+ except Exception as e:
+ capture_exception(error=e, context=("get_azure_id", None))
+ # We can fail by getting 404 from AZURE_ID_URL or failing validation
+ return response_error(
+ 4, message="Failed to generate Azure IDs. We looking into it."
+ )
+
+ canarydrop.cert_file_name = key["cert_file_name"]
+ canarydrop.app_id = key["app_id"]
+ canarydrop.cert = key["cert"]
+ canarydrop.tenant_id = key["tenant_id"]
+ canarydrop.cert_name = key["cert_name"]
+ canarydrop.token_url = f"{canary_http_channel}/{canarydrop.canarytoken.value()}"
+ save_canarydrop(canarydrop)
+ return AzureIDTokenResponse(
+ email=canarydrop.alert_email_recipient or "",
+ webhook_url=canarydrop.alert_webhook_url or "",
+ token=canarydrop.canarytoken.value(),
+ token_url=canarydrop.token_url,
+ auth_token=canarydrop.auth,
+ hostname=canarydrop.generated_hostname,
+ url_components=list(canarydrop.get_url_components()),
+ # additional information for Azure token response
+ cert_file_name=canarydrop.cert_file_name,
+ app_id=canarydrop.app_id,
+ cert=canarydrop.cert,
+ tenant_id=canarydrop.tenant_id,
+ cert_name=canarydrop.cert_name,
+ )
+
+
@create_response.register
def _(
token_request_details: CMDTokenRequest, canarydrop: Canarydrop
diff --git a/switchboard/switchboard.env.dist b/switchboard/switchboard.env.dist
index 162f43989..45023caf3 100644
--- a/switchboard/switchboard.env.dist
+++ b/switchboard/switchboard.env.dist
@@ -27,6 +27,8 @@ CANARY_ALERT_EMAIL_SUBJECT="Canarytoken"
CANARY_SENDGRID_API_KEY=""
CANARY_SENDGRID_SANDBOX_MODE=True
CANARY_AWSID_URL="https://not-in-use.com/test"
+CANARY_AZURE_ID_TOKEN_URL="https://not-in-use.com/test"
+CANARY_AZURE_ID_TOKEN_AUTH=""
CANARY_WG_PRIVATE_KEY_SEED=vk/GD+frlhve/hDTTSUvqpQ/WsQtioKAri0Rt5mg7dw=
CANARY_LISTEN_DOMAIN=127.0.0.1
diff --git a/templates/generate_new.html b/templates/generate_new.html
index 714659fce..e8692d12d 100644
--- a/templates/generate_new.html
+++ b/templates/generate_new.html
@@ -119,6 +119,7 @@
Web bug / URL token Alert when a URL is visited
DNS token Alert when a hostname is requested
AWS keys Alert when AWS key is used
+ Azure Login Certificate Azure Service Principal certificate that alerts when used to login with.
Sensitive command token Alert when a suspicious Windows command is run
Microsoft Word document Get alerted when a document is opened in Microsoft Word
Microsoft Excel document Get alerted when a document is opened in Microsoft Excel
@@ -163,6 +164,9 @@
{{ fileupload('signed_exe', text="Click to select an EXE or DLL for tokening") }}
+
+
+
@@ -771,6 +775,30 @@ Your AWS key token is active!
+
+
Your Azure Service Principal Login is active!
+
+
Save this json config file along with the certificate:
+
+ {{ textareacopydownload('result_azure_id', 'azure_id_config') }}
+
+
+
+
+
This canarytoken is triggered when someone uses this Service Principal Login to access Azure programmatically (through the API).
+
The Service Principal Login is unique. i.e. There is no chance of somebody guessing these credentials.
+
If this token fires, it is a clear indication that this Service Principal Login has "leaked".
+
Ideas for use:
+
+ Most systems have a ~/.azure folder (much like the ~/.aws or ~/.ssh). Create a config file with the config details from the token and place it
+ near the certificate (ensuring that the config value has a path to the certificate).
+ Place the credentials in private code repositories. If the token is triggered, it means that someone is accessing that repo without permission
+
+
+
+
Your Kubeconfig Token is active!
@@ -957,6 +985,17 @@
Your log4shell token is active!
return cmd.removeClass('error-outline').addClass('success-outline');
}
+ var checkAzureIdCertName= function() {
+ azure_id_cert_file_name = $('input[name=azure_id_cert_file_name]');
+ if (! azure_id_cert_file_name.parents('.field-azure_id').hasClass('hidden')) {
+ //only perform checks if field is visible. could be multiple checks
+ if (azure_id_cert_file_name.val().length == 0) {
+ return azure_id_cert_file_name.addClass('error-outline').removeClass('success-outline');
+ }
+ }
+ return azure_id_cert_file_name.removeClass('error-outline').addClass('success-outline');
+ }
+
var _checkSQLServerSelectedAction = function() {
sql_server_selected_action = $('#selected_sql_action');
@@ -1031,6 +1070,7 @@ Your log4shell token is active!
checkSignedExe();
checkClonedWebsite();
checkCmd();
+ checkAzureIdCertName();
checkSQLServer();
//invert UI elements, depending on whether there are errors or not
@@ -1085,6 +1125,10 @@ Your log4shell token is active!
var dd = new DropDown( $('#type'),
function(event) {
var list_item = $(event.target).parents('li');
+ if (!list_item.data('type')) {
+ event.preventDefault()
+ return;
+ }
$('#dropdown').data('selected', list_item.data('type'))
$('#selected_token').text(list_item.find('span.title').text());
ToggleOptionalFields(list_item.data('type'));
@@ -1212,6 +1256,23 @@ Your log4shell token is active!
});
}
+ var _handleAzureIDResponse = function(data){
+ const token_config_obj = {
+ appId: data['app_id'],
+ displayName: 'azure-cli-'+data['cert_name'],
+ fileWithCertAndPrivateKey: data['cert_file_name'],
+ password: null,
+ tenant: data['tenant_id'],
+ };
+ const token_config_JSON = JSON.stringify(token_config_obj, null, 2);
+ $('#result_azure_id').val(token_config_JSON);
+ $('#result_azure_id').css('height','160px').css('text-align','left').css('width','90%').css('font-size','.75rem');
+ $('a.file-download').each(function (i, e){
+ e = $(e);
+ e.prop('href', 'download?fmt='+e.data('fmt')+'&token='+data['token']+'&auth='+data['auth_token']);
+ });
+ }
+
var _handleKubeconfigResponse = function(data) {
$('a.file-download').each(function (i, e){
e = $(e);
@@ -1314,7 +1375,7 @@ Your log4shell token is active!
var _responseCallbacks = {'web': _handleWebResponse,
'dns': _handleDNSResponse,
'aws_keys':_handleAWSKeysResponse,
- // 'azure_id':_handleAzureIDResponse,
+ 'azure_id':_handleAzureIDResponse,
'web_image': _handleWebResponse,
'ms_word' : _handleFileDownloadResponse,
'ms_excel' : _handleFileDownloadResponse,
diff --git a/templates/history.html b/templates/history.html
index b760e5490..859970c84 100644
--- a/templates/history.html
+++ b/templates/history.html
@@ -111,6 +111,10 @@ Incident List is Currently Empty
IP: {{ canarydrop['triggered_list'][item]['src_ip'] }}
{% if canarydrop['type'] == 'aws_keys' %}
Channel: AWS API Key Token
+ {% elif canarydrop['type'] == 'cc' %}
+ Channel: Credit Card Token
+ {% elif canarydrop['type'] == 'azure_id' %}
+ Channel: Azure Login Certificate Token
{% else %}
Channel: {{ canarydrop['triggered_list'][item]['input_channel'] }}
{% endif %}
diff --git a/templates/manage_new.html b/templates/manage_new.html
index 9825be674..9a4f86134 100644
--- a/templates/manage_new.html
+++ b/templates/manage_new.html
@@ -167,7 +167,7 @@ Here's your AWS key token:
-
+
Here's your tokened Email address:
@@ -566,7 +566,7 @@
This token has {% if canarydrop.triggered_detail
var _handleFileDownloadResponse = function(data) {
$('a.file-download').each(function (i, e){
e = $(e);
- e.prop('href', 'download?fmt='+e.data('fmt')+'&token='+data['token']+'&auth='+data['auth']);
+ e.prop('href', 'download?fmt='+e.data('fmt')+'&token='+data['canarytoken']+'&auth='+data['auth']);
});
}
var _handleClonedWebsiteResponse = function(data) {
@@ -629,22 +629,22 @@ This token has {% if canarydrop.triggered_detail
$('#result_log4shell').val('${jndi:ldap://x${hostName}.L4J.'+data['generated_hostname']+'/a}');
}
- // var _handleAzureIDResponse = function(data){
- // const token_config_obj = {
- // appId: data['app_id'],
- // displayName: 'azure-cli-'+data['cert_name'],
- // fileWithCertAndPrivateKey: data['cert_file_name'],
- // password: null,
- // tenant: data['tenant_id'],
- // };
- // const token_config_JSON = JSON.stringify(token_config_obj, null, 2);
- // $('#result_azure_id').val(token_config_JSON);
- // $('#result_azure_id').css('height','160px').css('text-align','left').css('width','90%').css('font-size','.75rem');
- // $('a.file-download').each(function (i, e){
- // e = $(e);
- // e.prop('href', 'download?fmt='+e.data('fmt')+'&token='+data['canarytoken']+'&auth='+data['auth']);
- // });
- // }
+ var _handleAzureIDResponse = function(data){
+ const token_config_obj = {
+ appId: data['app_id'],
+ displayName: 'azure-cli-'+data['cert_name'],
+ fileWithCertAndPrivateKey: data['cert_file_name'],
+ password: null,
+ tenant: data['tenant_id'],
+ };
+ const token_config_JSON = JSON.stringify(token_config_obj, null, 2);
+ $('#result_azure_id').val(token_config_JSON);
+ $('#result_azure_id').css('height','160px').css('text-align','left').css('width','90%').css('font-size','.75rem');
+ $('a.file-download').each(function (i, e){
+ e = $(e);
+ e.prop('href', 'download?fmt='+e.data('fmt')+'&token='+data['canarytoken']+'&auth='+data['auth']);
+ });
+ }
var _handleDefaultResponse = function(data) {
return;
@@ -653,7 +653,7 @@ This token has {% if canarydrop.triggered_detail
var _responseCallbacks = {'web': _handleWebResponse,
'dns': _handleDNSResponse,
'aws_keys': _handleAWSKeysResponse,
- // 'azure_id':_handleAzureIDResponse,
+ 'azure_id':_handleAzureIDResponse,
'web_image': _handleWebResponse,
'ms_word' : _handleFileDownloadResponse,
'ms_excel' : _handleFileDownloadResponse,
@@ -686,9 +686,9 @@ This token has {% if canarydrop.triggered_detail
}
cb(data)
- $('.jumbotron').css('height', $('.success').innerHeight())
+ // $('.jumbotron').css('height', $('.success').innerHeight())
- $('.manage-link > a').prop('href', '/manage?token='+data['Token']+'&auth='+data['Auth']);
+ $('.manage-link > a').prop('href', '/manage?token='+data['token']+'&auth='+data['auth_token']);
}
displayToken(canarydrop);
diff --git a/tests/integration/test_azure_key_token.py b/tests/integration/test_azure_key_token.py
new file mode 100644
index 000000000..6f2a1828f
--- /dev/null
+++ b/tests/integration/test_azure_key_token.py
@@ -0,0 +1,113 @@
+import os
+from distutils.util import strtobool
+
+from typing import Union
+from pydantic import HttpUrl
+import pytest
+
+from canarytokens.models import (
+ V2,
+ V3,
+ AzureIDTokenHistory,
+ AzureIDTokenRequest,
+ AzureIDTokenResponse,
+ Memo,
+ TokenTypes,
+)
+from tests.utils import azure_token_fire, create_token
+from tests.utils import get_token_history
+from tests.utils import run_or_skip, v2, v3
+
+
+@pytest.mark.skipif(
+ strtobool(os.getenv("SKIP_AZURE_KEY_TEST", "True")),
+ reason="avoid using up an Azure user each time we run tests",
+)
+@pytest.mark.parametrize(
+ "version",
+ [
+ v2,
+ v3,
+ ],
+)
+@pytest.mark.parametrize(
+ "data,expected_hit",
+ [
+ (
+ {
+ "app_id": "some-app-id",
+ "cert_id": "some-cert-id",
+ "auth_details": [
+ {
+ "key": "Azure AD App Authentication Library",
+ "value": "Family: MSAL Library: MSAL.Python 1.20.0 Platform: Python",
+ }
+ ],
+ "ip": "1.2.3.4",
+ "location": {
+ "city": "Pretoria",
+ "state": "Gauteng",
+ "countryOrRegion": "ZA",
+ "geoCoordinates": {"latitude": -25.73, "longitude": 28.21},
+ },
+ "resource": "Windows Azure Service Management API",
+ "tenant_id": "some-tenant-id",
+ "time": "2023-04-03T15:40:13.785374Z",
+ },
+ {
+ "src_ip": "1.2.3.4",
+ "additional_info": {
+ "coordinates": {"latitude": ["-25.73"], "longitude": ["28.21"]},
+ "azure_key_log_data": {
+ "Date": ["2023-04-03T15:40:13.785374Z"],
+ "Authentication": [
+ "\nAzure AD App Authentication Library: Family: MSAL Library: MSAL.Python 1.20.0 Platform: Python"
+ ],
+ },
+ "location": {
+ "city": ["Pretoria"],
+ "state": ["Gauteng"],
+ "countryOrRegion": ["ZA"],
+ },
+ "microsoft_azure": {
+ "App ID": ["some-app-id"],
+ "Resource": ["Windows Azure Service Management API"],
+ "Cert ID": ["some-cert-id"],
+ },
+ },
+ "input_channel": "HTTP",
+ },
+ )
+ ],
+)
+def test_azure_token_post_request_processing(
+ data: dict, expected_hit: dict, version: Union[V2, V3], runv2: bool, runv3: bool
+): # pragma: no cover
+ """
+ When an Azure Token is triggered azure makes a POST request
+ back to the http channel. This is mimicked here using `azure_token_fire`.
+ """
+ run_or_skip(version=version, runv2=runv2, runv3=runv3)
+ token_request = AzureIDTokenRequest(
+ webhook_url=HttpUrl(
+ "https://webhook.site/873f846e-9434-4db9-bfb4-1e7f60464f97", scheme="https"
+ ),
+ memo=Memo("Azure test token"),
+ azure_id_cert_file_name="test_token.pem",
+ )
+ token_resp = create_token(
+ token_request=token_request,
+ version=version,
+ )
+ token_info = AzureIDTokenResponse(**token_resp)
+ azure_token_fire(token_info=token_info, data=data, version=version)
+
+ token_hist_resp = get_token_history(token_info=token_info, version=version)
+ token_hist = AzureIDTokenHistory(**token_hist_resp)
+ assert len(token_hist.hits) == 1
+ hit = token_hist.hits[0]
+ assert token_hist.hits[0]
+ assert hit.token_type == TokenTypes.AZURE_KEYS
+ hit_dict = hit.dict()
+ for key in expected_hit:
+ assert hit_dict[key] == expected_hit[key]
diff --git a/tests/utils.py b/tests/utils.py
index 2d8239d87..8027a5e3b 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -23,6 +23,7 @@
AnyTokenResponse,
AWSKeyAdditionalInfo,
AWSKeyTokenResponse,
+ AzureIDTokenResponse,
CustomBinaryTokenRequest,
CustomBinaryTokenResponse,
CustomImageTokenRequest,
@@ -247,6 +248,28 @@ def aws_token_fire(token_info: AWSKeyTokenResponse, version: Union[V2, V3]) -> N
_ = urllib.request.urlopen(req)
+def azure_token_fire(
+ token_info: AzureIDTokenResponse, data: dict, version: Union[V2, V3]
+) -> None:
+ """Triggers an Azure token via the HTTP channel.
+ This mimics the POST we receive.
+
+ Args:
+ token_info (AzureIDTokenResponse): This is the token that gets triggered.
+ data (dict): the data that would be passed as the body
+ """
+ if version.live:
+ url = token_info.token_url
+ else:
+ # Need to hit Switchboard directly.
+ http_url = parse_obj_as(HttpUrl, token_info.token_url)
+ http_url.port = version.canarytokens_http_port
+ url = f"{http_url.scheme}://{http_url.host}:{http_url.port}{http_url.path}"
+
+ resp = requests.post(url, json=data)
+ resp.raise_for_status()
+
+
@retry_on_failure(retry_when_raised=(requests.exceptions.HTTPError,))
def get_token_history(
token_info: Union[
From 3ab203a837fbb31dc80d52947a9e4b0008a32f09 Mon Sep 17 00:00:00 2001
From: William Leighton Dawson
Date: Tue, 11 Apr 2023 17:52:08 +0200
Subject: [PATCH 2/5] fix test utils, azure_key -> azure_id in most places
---
canarytokens/channel_http.py | 4 ++--
canarytokens/models.py | 20 ++++++++++----------
canarytokens/tokens.py | 6 +++---
frontend/app.py | 2 +-
tests/integration/test_azure_key_token.py | 6 +++---
tests/units/test_frontend.py | 9 +--------
tests/utils.py | 22 ++++++++++++++++++++++
7 files changed, 42 insertions(+), 27 deletions(-)
diff --git a/canarytokens/channel_http.py b/canarytokens/channel_http.py
index 2b78b1268..b4a82aa6c 100644
--- a/canarytokens/channel_http.py
+++ b/canarytokens/channel_http.py
@@ -127,8 +127,8 @@ def render_POST(self, request: Request):
canarydrop.add_canarydrop_hit(token_hit=token_hit)
self.dispatch(canarydrop=canarydrop, token_hit=token_hit)
return b"success"
- elif canarydrop.type == TokenTypes.AZURE_KEYS:
- token_hit = Canarytoken._parse_azure_key_trigger(request)
+ elif canarydrop.type == TokenTypes.AZURE_ID:
+ token_hit = Canarytoken._parse_azure_id_trigger(request)
canarydrop.add_canarydrop_hit(token_hit=token_hit)
self.dispatch(canarydrop=canarydrop, token_hit=token_hit)
return b"success"
diff --git a/canarytokens/models.py b/canarytokens/models.py
index f8fda3f77..0b67210d4 100644
--- a/canarytokens/models.py
+++ b/canarytokens/models.py
@@ -296,7 +296,7 @@ class TokenTypes(str, enum.Enum):
SQL_SERVER = "sql_server"
MY_SQL = "my_sql"
AWS_KEYS = "aws_keys"
- AZURE_KEYS = "azure_id"
+ AZURE_ID = "azure_id"
SIGNED_EXE = "signed_exe"
FAST_REDIRECT = "fast_redirect"
SLOW_REDIRECT = "slow_redirect"
@@ -429,7 +429,7 @@ class AWSKeyTokenRequest(TokenRequest):
class AzureIDTokenRequest(TokenRequest):
- token_type: Literal[TokenTypes.AZURE_KEYS] = TokenTypes.AZURE_KEYS
+ token_type: Literal[TokenTypes.AZURE_ID] = TokenTypes.AZURE_ID
azure_id_cert_file_name: str
def v2_dict(self) -> Dict[str, Any]:
@@ -705,7 +705,7 @@ class AWSKeyTokenResponse(TokenResponse):
class AzureIDTokenResponse(TokenResponse):
- token_type: Literal[TokenTypes.AZURE_KEYS] = TokenTypes.AZURE_KEYS
+ token_type: Literal[TokenTypes.AZURE_ID] = TokenTypes.AZURE_ID
app_id: str
tenant_id: str
cert: str
@@ -1089,8 +1089,8 @@ def normalize_additional_info_names(cls, values: dict[str, Any]) -> dict[str, An
return {k.lower(): v for k, v in values.items()}
-class AzureKeyAdditionalInfo(BaseModel):
- azure_key_log_data: dict[str, list[str]]
+class AzureIDAdditionalInfo(BaseModel):
+ azure_id_log_data: dict[str, list[str]]
microsoft_azure: dict[str, list[str]]
location: dict[str, list[str]]
coordinates: dict[str, list[str]]
@@ -1099,7 +1099,7 @@ class AzureKeyAdditionalInfo(BaseModel):
def normalize_additional_info_names(cls, values: dict[str, Any]) -> dict[str, Any]: # type: ignore
keys_to_convert = [
# TODO: make this consistent.
- ("Azure ID Log Data", "azure_key_log_data"),
+ ("Azure ID Log Data", "azure_id_log_data"),
("Microsoft Azure", "microsoft_azure"),
("Location", "location"),
("Coordinates", "coordinates"),
@@ -1119,7 +1119,7 @@ def serialize_for_v2(self) -> dict:
data = self.dict()
keys_to_convert = [
# TODO: make this consistent.
- ("Azure ID Log Data", "azure_key_log_data"),
+ ("Azure ID Log Data", "azure_id_log_data"),
("Microsoft Azure", "microsoft_azure"),
("Location", "location"),
("Coordinates", "coordinates"),
@@ -1227,8 +1227,8 @@ def adjust_geo_info(cls, value):
class AzureIDTokenHit(TokenHit):
- token_type: Literal[TokenTypes.AZURE_KEYS] = TokenTypes.AZURE_KEYS
- additional_info: Optional[AzureKeyAdditionalInfo]
+ token_type: Literal[TokenTypes.AZURE_ID] = TokenTypes.AZURE_ID
+ additional_info: Optional[AzureIDAdditionalInfo]
class Config:
allow_population_by_field_name = True
@@ -1549,7 +1549,7 @@ class AWSKeyTokenHistory(TokenHistory[AWSKeyTokenHit]):
class AzureIDTokenHistory(TokenHistory):
- token_type: Literal[TokenTypes.AZURE_KEYS] = TokenTypes.AZURE_KEYS
+ token_type: Literal[TokenTypes.AZURE_ID] = TokenTypes.AZURE_ID
hits: List[AzureIDTokenHit]
diff --git a/canarytokens/tokens.py b/canarytokens/tokens.py
index 31443f742..66214c7a5 100644
--- a/canarytokens/tokens.py
+++ b/canarytokens/tokens.py
@@ -404,10 +404,10 @@ def _parse_aws_key_trigger(
return AWSKeyTokenHit(**hit_info)
@staticmethod
- def _parse_azure_key_trigger(
+ def _parse_azure_id_trigger(
request: Request,
) -> AzureIDTokenHit:
- """When an AzureKey token is triggered a lambda makes a POST request
+ """When an AzureID token is triggered, Azure makes a POST request
back to switchboard. The `request` is processed, fields extracted,
and an `AzureIDTokenHit` is created.
@@ -456,7 +456,7 @@ def _parse_azure_key_trigger(
},
}
hit_info = {
- "token_type": TokenTypes.AZURE_KEYS,
+ "token_type": TokenTypes.AZURE_ID,
"time_of_hit": hit_time,
"input_channel": INPUT_CHANNEL_HTTP,
"src_ip": src_ip,
diff --git a/frontend/app.py b/frontend/app.py
index d549c672e..f6e4a369c 100644
--- a/frontend/app.py
+++ b/frontend/app.py
@@ -993,7 +993,7 @@ def _create_aws_key_token_response(
@create_response.register
-def _create_azure_key_token_response(
+def _create_azure_id_token_response(
token_request_details: AzureIDTokenRequest,
canarydrop: Canarydrop,
settings: Optional[Settings] = None,
diff --git a/tests/integration/test_azure_key_token.py b/tests/integration/test_azure_key_token.py
index 6f2a1828f..53d03bd6c 100644
--- a/tests/integration/test_azure_key_token.py
+++ b/tests/integration/test_azure_key_token.py
@@ -20,7 +20,7 @@
@pytest.mark.skipif(
- strtobool(os.getenv("SKIP_AZURE_KEY_TEST", "True")),
+ strtobool(os.getenv("SKIP_AZURE_ID_TEST", "True")),
reason="avoid using up an Azure user each time we run tests",
)
@pytest.mark.parametrize(
@@ -58,7 +58,7 @@
"src_ip": "1.2.3.4",
"additional_info": {
"coordinates": {"latitude": ["-25.73"], "longitude": ["28.21"]},
- "azure_key_log_data": {
+ "azure_id_log_data": {
"Date": ["2023-04-03T15:40:13.785374Z"],
"Authentication": [
"\nAzure AD App Authentication Library: Family: MSAL Library: MSAL.Python 1.20.0 Platform: Python"
@@ -107,7 +107,7 @@ def test_azure_token_post_request_processing(
assert len(token_hist.hits) == 1
hit = token_hist.hits[0]
assert token_hist.hits[0]
- assert hit.token_type == TokenTypes.AZURE_KEYS
+ assert hit.token_type == TokenTypes.AZURE_ID
hit_dict = hit.dict()
for key in expected_hit:
assert hit_dict[key] == expected_hit[key]
diff --git a/tests/units/test_frontend.py b/tests/units/test_frontend.py
index cf0828f90..d0453aeae 100644
--- a/tests/units/test_frontend.py
+++ b/tests/units/test_frontend.py
@@ -155,14 +155,7 @@ def test_creating_all_tokens(
test_client: TestClient,
setup_db: None,
) -> None:
- token_request = token_request_type(
- email="test@test.com",
- webhook_url="https://hooks.slack.com/test",
- memo="test stuff break stuff fix stuff test stuff",
- redirect_url="https://youtube.com",
- clonedsite="https://test.com",
- cmd_process_name="klist.exe",
- )
+ token_request = get_token_request(token_request_type)
try:
resp = test_client.post(
diff --git a/tests/utils.py b/tests/utils.py
index 8027a5e3b..532e9496b 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -24,6 +24,7 @@
AWSKeyAdditionalInfo,
AWSKeyTokenResponse,
AzureIDTokenResponse,
+ AzureIDAdditionalInfo,
CustomBinaryTokenRequest,
CustomBinaryTokenResponse,
CustomImageTokenRequest,
@@ -460,6 +461,7 @@ def get_token_request(token_request_type: AnyTokenRequest) -> AnyTokenRequest:
redirect_url="https://youtube.com",
clonedsite="https://test.com",
cmd_process_name="klist.exe",
+ azure_id_cert_file_name="test.pem",
)
@@ -480,6 +482,26 @@ def get_basic_hit(token_type: TokenTypes) -> AnyTokenHit:
"last_used": ["2022-07-29T05:48:00+00:00"],
}
)
+ elif token_type == TokenTypes.AZURE_ID:
+ additional_info = AzureIDAdditionalInfo(
+ coordinates={"latitude": ["-25.73"], "longitude": ["28.21"]},
+ azure_id_log_data={
+ "Date": ["2023-04-03T15:40:13.785374Z"],
+ "Authentication": [
+ "\nAzure AD App Authentication Library: Family: MSAL Library: MSAL.Python 1.20.0 Platform: Python"
+ ],
+ },
+ location={
+ "city": ["Pretoria"],
+ "state": ["Gauteng"],
+ "countryOrRegion": ["ZA"],
+ },
+ microsoft_azure={
+ "App ID": ["some-app-id"],
+ "Resource": ["Windows Azure Service Management API"],
+ "Cert ID": ["some-cert-id"],
+ },
+ )
else:
additional_info = AdditionalInfo()
generic_hit = dict(
From 7a73fabaaae95c1cf1a3b31cbcb0ca5634a1963d Mon Sep 17 00:00:00 2001
From: William Leighton Dawson
Date: Tue, 11 Apr 2023 19:53:45 +0200
Subject: [PATCH 3/5] exclude aws and azure tokens from auto-generation in
frontend tests
We don't want to use up creds on every run, and the tokens have been tested manually.
---
.github/workflows/test.yml | 6 +++---
tests/units/test_frontend.py | 8 +++++++-
2 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ea438f555..fc0a9c224 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -111,7 +111,7 @@ jobs:
export CANARY_MAILGUN_API_KEY=${{ secrets.TESTING_MAILGUN_API_KEY }}
export CANARY_SENTRY_ENVIRONMENT=ci
cd tests
- poetry run coverage run --source=../canarytokens --omit="integration/test_custom_binary.py,integration/test_sql_server_token.py" -m pytest units --runv3
+ poetry run coverage run --source=../canarytokens --omit="integration/test_custom_binary.py,integration/test_sql_server_token.py" -m pytest units --runv3 -v
- name: Check coverage is over threshold percentage for unit tests
# Here we check coverage info on all tests
@@ -128,13 +128,13 @@ jobs:
sleep 10
export TEST_NETWORK=`docker network ls | grep git | python -c "from sys import stdin; print(stdin.read().split()[1])"`
export TEST_HOST=`docker network inspect $TEST_NETWORK | jq '.[0].IPAM.Config[0].Gateway' | sed 's/"//g'`
- LIVE=False poetry run coverage run --source=../canarytokens --omit=integration/test_custom_binary.py -m pytest integration --runv3
+ LIVE=False poetry run coverage run --source=../canarytokens --omit=integration/test_custom_binary.py -m pytest integration --runv3 -v
- name: Run integration tests (against V2)
# Here we gather coverage info on integration tests.
run: |
cd tests
- LIVE=True poetry run coverage run --source=integration --omit=integration/test_custom_binary.py -m pytest integration --runv2
+ LIVE=True poetry run coverage run --source=integration --omit=integration/test_custom_binary.py -m pytest integration --runv2 -v
- name: Check coverage is over threshold percentage for integration tests
# Here we check coverage info that all integration tests where indeed run.
diff --git a/tests/units/test_frontend.py b/tests/units/test_frontend.py
index d0453aeae..e3aadc327 100644
--- a/tests/units/test_frontend.py
+++ b/tests/units/test_frontend.py
@@ -14,6 +14,8 @@
AnyTokenResponse,
AWSKeyTokenRequest,
AWSKeyTokenResponse,
+ AzureIDTokenRequest,
+ AzureIDTokenResponse,
BrowserScannerSettingsRequest,
CCTokenRequest,
CCTokenResponse,
@@ -131,11 +133,15 @@ 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,
+ AWSKeyTokenRequest, # don't use up an AWS key
+ AzureIDTokenRequest, # don't use up an Azure ID
+ CCTokenRequest, # don't use up a CC
CustomImageTokenRequest,
CustomBinaryTokenRequest,
]
set_of_unsupported_response_classes = [
+ AWSKeyTokenResponse,
+ AzureIDTokenResponse,
CCTokenResponse,
CustomImageTokenResponse,
CustomBinaryTokenResponse,
From d1acac75bc0750424c17eeb020c3826d45843949 Mon Sep 17 00:00:00 2001
From: William Leighton Dawson
Date: Wed, 12 Apr 2023 11:51:02 +0200
Subject: [PATCH 4/5] exclude code from coverage
---
canarytokens/authenticode.py | 4 +++-
canarytokens/azurekeys.py | 2 +-
makefile | 18 +++++++++---------
3 files changed, 13 insertions(+), 11 deletions(-)
diff --git a/canarytokens/authenticode.py b/canarytokens/authenticode.py
index 0be1c62a4..ff55f0963 100644
--- a/canarytokens/authenticode.py
+++ b/canarytokens/authenticode.py
@@ -4,7 +4,9 @@
from canarytokens.sign_file import authenticode_sign_binary
-def make_canary_authenticode_binary(nxdomain_token_url: str, filebody: bytes) -> bytes:
+def make_canary_authenticode_binary( # pragma: no cover
+ nxdomain_token_url: str, filebody: bytes
+) -> bytes:
"""Takes in a nxdomain url (eg: http://{token}.nxdomain.tools) and bytes string (some binary to sign)
and returns bytes (the signed binary).
diff --git a/canarytokens/azurekeys.py b/canarytokens/azurekeys.py
index 7e398bf4d..b8c6db696 100644
--- a/canarytokens/azurekeys.py
+++ b/canarytokens/azurekeys.py
@@ -8,7 +8,7 @@
from canarytokens import tokens
-def get_azure_id(
+def get_azure_id( # pragma: no cover
token: tokens.Canarytoken,
server: str,
cert_file_name: str,
diff --git a/makefile b/makefile
index 0b00ff274..17ab4ea9c 100644
--- a/makefile
+++ b/makefile
@@ -10,29 +10,29 @@ frontend:
.PHONY: testv3
testv3:
- cd tests
+ cd tests; \
TEST_HOST=`docker network inspect canarytokens_devcontainer_default | jq '.[0].Containers | to_entries[].value | select(.Name == "canarytokens_devcontainer-app-1").IPv4Address' | sed -E 's/"//g; s/\/[0-9]+//'` \
- poetry run coverage run --source=../canarytokens -m pytest . --runv3 -v
+ poetry run coverage run --source=../canarytokens -m pytest . --runv3 -v; \
poetry run coverage report -m
.PHONY: testv3live
testv3live:
- cd tests
- CANARY_CHANNEL_MYSQL_PORT=3306 TEST_HOST=jingwei.tools LIVE=True poetry run coverage run --source=integration -m pytest integration --runv3
- poetry run coverage report -m
+ cd tests; \
+ CANARY_CHANNEL_MYSQL_PORT=3306 TEST_HOST=jingwei.tools LIVE=True poetry run coverage run --source=integration -m pytest integration --runv3; \
+ poetry run coverage report -m; \
.PHONY: unitsv3
unitsv3:
- cd tests
- poetry run coverage run --source=../canarytokens -m pytest units --runv3
+ cd tests; \
+ poetry run coverage run --source=../canarytokens -m pytest units --runv3 -v; \
poetry run coverage report -m
.PHONY: testv2
testv2:
- cd tests
+ cd tests; \
poetry run pytest integration --runv2 --pdb
.PHONY: testv2-s
testv2-s:
- cd tests
+ cd tests; \
poetry run pytest -s integration --runv2 --pdb
From 1bea9ae696d6190f45fa0b2b055f6d0572dababa Mon Sep 17 00:00:00 2001
From: William Leighton Dawson
Date: Wed, 12 Apr 2023 16:17:02 +0200
Subject: [PATCH 5/5] add more exclusions
---
canarytokens/authenticode.py | 4 ++--
canarytokens/azurekeys.py | 4 ++--
canarytokens/extendtoken.py | 18 ++++++++++--------
canarytokens/sign_file.py | 2 +-
4 files changed, 15 insertions(+), 13 deletions(-)
diff --git a/canarytokens/authenticode.py b/canarytokens/authenticode.py
index ff55f0963..187aedcd8 100644
--- a/canarytokens/authenticode.py
+++ b/canarytokens/authenticode.py
@@ -4,9 +4,9 @@
from canarytokens.sign_file import authenticode_sign_binary
-def make_canary_authenticode_binary( # pragma: no cover
+def make_canary_authenticode_binary(
nxdomain_token_url: str, filebody: bytes
-) -> bytes:
+) -> bytes: # pragma: no cover
"""Takes in a nxdomain url (eg: http://{token}.nxdomain.tools) and bytes string (some binary to sign)
and returns bytes (the signed binary).
diff --git a/canarytokens/azurekeys.py b/canarytokens/azurekeys.py
index b8c6db696..61bc7059c 100644
--- a/canarytokens/azurekeys.py
+++ b/canarytokens/azurekeys.py
@@ -8,7 +8,7 @@
from canarytokens import tokens
-def get_azure_id( # pragma: no cover
+def get_azure_id(
token: tokens.Canarytoken,
server: str,
cert_file_name: str,
@@ -17,7 +17,7 @@ def get_azure_id( # pragma: no cover
cert: Optional[str] = None,
tenant_id: Optional[str] = None,
cert_name: Optional[str] = None,
-) -> AzureID:
+) -> AzureID: # pragma: no cover
if app_id and cert and tenant_id and cert_name:
return AzureID(
**{
diff --git a/canarytokens/extendtoken.py b/canarytokens/extendtoken.py
index c3d7f5716..61cb0c745 100644
--- a/canarytokens/extendtoken.py
+++ b/canarytokens/extendtoken.py
@@ -30,7 +30,7 @@ def __init__(
password,
card_name,
token=None,
- ):
+ ): # pragma: no cover
self.email = email
self.token = token
self.kind = "AMEX"
@@ -45,7 +45,9 @@ def __init__(
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:
+ def _post_api(
+ self, endpoint: str, data: Optional[str] = None
+ ) -> requests.Response: # pragma: no cover
"""Performs a POST against the passed endpoint with the data passed"""
headers = {
"Content-Type": "application/json",
@@ -163,7 +165,7 @@ def get_virtual_cards(self) -> list[tuple[str, str]]: # pragma: no cover
)
return cards
- def get_card_info(self, card_id) -> Optional[dict[str, str]]:
+ def get_card_info(self, card_id) -> Optional[dict[str, str]]: # pragma: no cover
"""Returns all the data about a passed card_id available"""
req = self._get_api("https://v.paywithextend.com/virtualcards/" + card_id)
return req.json()
@@ -228,7 +230,7 @@ def make_card(
address: str,
billing_zip: str,
limit_cents: int = 100,
- ) -> CreditCard:
+ ) -> CreditCard: # pragma: no cover
"""Creates a new CreditCard via Extend's CreateVirtualCard API"""
cc = self.get_parent_card_id()
now_ts = datetime.datetime.now() - datetime.timedelta(days=1)
@@ -283,7 +285,7 @@ def create_credit_card(
last_name: Optional[str] = None,
address: Optional[str] = None,
billing_zip: Optional[str] = None,
- ) -> CreditCard:
+ ) -> CreditCard: # pragma: no cover
"""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:
@@ -305,13 +307,13 @@ def create_credit_card(
return cc
- def get_credit_card(self, id: str) -> CreditCard:
+ def get_credit_card(self, id: str) -> CreditCard: # pragma: no cover
"""Abstract method to get a virtual credit card"""
pass
- def get_transaction_events( # noqa C901 # pragma: no cover
+ def get_transaction_events( # noqa C901
self, since: Optional[datetime.datetime] = None
- ) -> list:
+ ) -> list: # pragma: no cover
"""Returns a list of recent transactions for the org"""
txns = []
req = self._get_api("https://api.paywithextend.com/events")
diff --git a/canarytokens/sign_file.py b/canarytokens/sign_file.py
index 2f310e82e..e685782a4 100644
--- a/canarytokens/sign_file.py
+++ b/canarytokens/sign_file.py
@@ -7,7 +7,7 @@
def authenticode_sign_binary(
nxdomain_token_url: str, inputfile: Path, outputfile: Path
-):
+): # pragma: no cover
try:
tmpdir = tempfile.mkdtemp()