├── canary_alert_extract.sh ├── LICENSE ├── canarygen_awscreds.cmd ├── canarygen_awscreds_auto.py ├── .gitignore ├── canarygen_awscreds_auto.sh ├── delete_tokens.py ├── alert_management.sh ├── CreateTokens.ps1 ├── canary_api2csv.sh ├── DeployTokens.ps1 ├── deploy_tokens.ps1 ├── README.md └── canaryconsole.py /canary_alert_extract.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Create a CSV with the last week's worth of alerts from your Canary console 3 | # Requires curl and jq to be in the path 4 | 5 | # Set this variable to your API token 6 | export token=deadbeef12345678 7 | 8 | # Customize this variable to match your console URL 9 | export console=ab123456.canary.tools 10 | 11 | # Date format (one week ago) 12 | export dateformat=`date -v-1w "+%Y-%m-%d-%H:%M:%S"` 13 | 14 | # Filename date (right now) 15 | export filedate=`date "+%Y%m%d%H%M%S"` 16 | 17 | # Complete Filename 18 | export filename=$filedate-$console-1week-alert-export.csv 19 | 20 | # Base URL 21 | export baseurl="https://$console/api/v1/incidents/all?auth_token=$token&shrink=true&newer_than" 22 | 23 | # Run the jewels 24 | echo Datetime,Alert Description,Target,Target Port,Attacker,Attacker RevDNS > $filename 25 | curl "$baseurl=$dateformat" | jq -r '.incidents[] | [.description | .created_std, .description, .dst_host, .dst_port, .src_host, .src_host_reverse | tostring] | @csv' >> $filename 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Thinkst Applied Research 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /canarygen_awscreds.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM Test script to generate AWS creds 3 | 4 | REM Requires curl and jq. Customize name/path to EXEs below. 5 | set curl=curl 6 | set jq=jq-win64.exe 7 | 8 | REM Grab the date and time for creating unique files 9 | for /f "tokens=1,2,3,4 delims=/ " %%a in ('date /t') do set currdate=%%d%%c%%b 10 | for /f "tokens=1,2,3,4 delims=.:" %%a in ("%time%") do set currtime=%%a%%b%%c 11 | 12 | REM Set this variable to your API token 13 | set token=abcdef123456789 14 | 15 | REM Customize this variable to match your console URL 16 | set console=ab123456.canary.tools 17 | ECHO Using console %console% 18 | 19 | REM Token memo 20 | set tokenmemo=\"Consider any AWS creds from %USERNAME% on %COMPUTERNAME% compromised\" 21 | 22 | REM Base URL 23 | set baseurl="https://$console/api/v1/canarytoken/create?auth_token=%token%&memo=%tokenmemo%&kind=aws-id&aws_id_username=%USERNAME%" 24 | 25 | REM Run the jewels 26 | ECHO Creating token. One moment... 27 | %curl% -s -X POST https://%console%/api/v1/canarytoken/create -d "auth_token=%token%&memo=%tokenmemo%&kind=aws-id" | %jq% -r ".canarytoken.renders.\"aws-id\"" > awscreds_%currdate%%currtime%.txt 28 | 29 | ECHO New AWS Credentials Canarytoken written to file awscreds_%currdate%%currtime%.txt 30 | pause 31 | -------------------------------------------------------------------------------- /canarygen_awscreds_auto.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Generate AWS Creds 0.1 4 | # canarygen_awscreds.py 5 | # 6 | # This is the "auto" version of this script. Run it unattended and it will 7 | # automatically grab username and hostname variables from the system it is 8 | # run on. 9 | # 10 | # PREREQS (works with python 2.7 and 3.x) 11 | #pip -q install canarytools 12 | #pip3 -q install canarytools 13 | import canarytools 14 | import getpass 15 | import socket 16 | import datetime 17 | 18 | # Prep canarytools - the first parameter is the CName for your console (e.g. 19 | # the first part of ab1234ef.canary.tools) and the second is your Canary 20 | # Console API key. 21 | console = canarytools.Console("ab1234ef","deadbeef02082f1ad8bbc9cdfbfffeef") 22 | 23 | # By default, this script uses the current user and system as the token memo 24 | # The memo can be manually changed in the Canary Console later on. Otherwise, 25 | # customize tokenmemo's text below. 26 | username = getpass.getuser() 27 | hostname = socket.gethostname() 28 | tokenmemo = "Fake AWS creds: {} on {}".format(username, hostname) 29 | 30 | # Create AWS Creds token 31 | result = console.tokens.create(memo=tokenmemo, kind=canarytools.CanaryTokenKinds.AWS) 32 | print("[default]") 33 | print(result.access_key_id) 34 | print(result.secret_access_key) 35 | 36 | # Pull AWS Creds token into a file 37 | currdatetime = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") 38 | filename = "awscreds_{}.txt".format(currdatetime) 39 | print("\nWriting file to {}".format(filename)) 40 | f= open(filename,"w+") 41 | f.write("[default]\n") 42 | f.write("aws_access_key_id = {}\n".format(result.access_key_id)) 43 | f.write("aws_secret_access_key = {}\n".format(result.secret_access_key)) 44 | f.close() 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /canarygen_awscreds_auto.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Generate AWS Creds 0.1 3 | # canarygen_awscreds.sh 4 | 5 | # This is the "auto" version of this script. Run it unattended and it will 6 | # automatically grab username and hostname variables from the system it is 7 | # run on. 8 | 9 | # Set the following variables to the correct values for your: 10 | # 1. Unique Canary Console URL 11 | # 2. Canary Console API Key 12 | # 3. Memo/reminder for the token (this is what you see when you get an alert!) 13 | # 4. Flock Name 14 | # 5. Path where file will be created (defaults to root of home directory) 15 | export console=ab1234ef.canary.tools 16 | export token=deadbeef02082f1ad8bbc9cdfbfffeef 17 | export tokenmemo="Fake AWS Creds on host: $HOSTNAME username: $USER" 18 | export flock='Default Flock' 19 | export filepath=~ 20 | 21 | # Get current date for part of file name 22 | export filedate=`date "+%Y%m%d%H%M%S"` 23 | 24 | # Get FlockID from flock name 25 | flockid=$(curl -s "https://${console}/api/v1/flocks/filter?auth_token=${token}&filter_str=${flock}" | grep -Eo '"flock_id":.*?[^\\]",' | awk -F '[":"]' '{print $5,$6}' OFS=":") 26 | #echo -e "\nFlockID is $flockid" 27 | 28 | # Create the token 29 | #echo -e "Creating token" 30 | awscreds=$(curl -s https://$console/api/v1/canarytoken/create \ 31 | -d auth_token=$token \ 32 | -d memo="$tokenmemo" \ 33 | -d kind=aws-id \ 34 | -d flock_id=$flockid | grep -Eo '"aws-id":.*?[^\\]",' | awk -F '[":"},]' '{print $5,$6}') 35 | 36 | # Write the token to a local text file 37 | echo -e "$awscreds" > $filepath/awscreds_$filedate.txt 38 | echo -e "\nCreds written to $filepath/awscreds_$filedate.txt" 39 | 40 | # for security reasons, we should unset/wipe all variables that contained an auth token, or evidence of Canary/Canarytokens 41 | unset console 42 | unset token 43 | unset tokenmemo 44 | unset flock 45 | unset ping 46 | unset flockid 47 | 48 | exit 49 | -------------------------------------------------------------------------------- /delete_tokens.py: -------------------------------------------------------------------------------- 1 | # Delete Tokens 2 | # delete_tokens.py 3 | # Authors: Jay wrote most of this and Adrian updated to Python3 4 | # 5 | # Be VERY careful with this script! It is designed to wipe out all tokens 6 | # after some heavy automated API testing. Don't use it if you have any 7 | # production tokens that you've worked hard to create and deploy - you'll 8 | # lose them and will have to redeploy! 9 | # 10 | 11 | import requests 12 | import sys 13 | import re 14 | 15 | def main(args): 16 | if len(args) < 2: 17 | print("usage: python delete_tokens.py ") 18 | auth = args[1] 19 | console = args[0] 20 | get_url = "{base}/api/v1/canarytokens/fetch?auth_token={auth}".format( 21 | base=console, auth=auth) 22 | resp = requests.get(get_url) 23 | resp_obj = resp.json() 24 | print("Current tokens on your console") 25 | print("-----------------------------------------------------------------------------------") 26 | print("kind\t\ttoken\t\t\t\t\tmemo") 27 | print("-----------------------------------------------------------------------------------") 28 | for token in resp_obj['tokens']: 29 | print("{}\t\t{}\t\t{}".format(token['kind'], token['canarytoken'], token['memo'])) 30 | print("-----------------------------------------------------------------------------------") 31 | canarytoken = input("Are you sure you would like to delete all your Canarytokens? [Y\\n] PLEASE NOTE: This is irreversible! ") 32 | if canarytoken != 'Y': 33 | print("Not deleting any canarytokens from your Canary console.") 34 | exit(0) 35 | delete_url = "{base}/api/v1/canarytoken/delete".format(base=console) 36 | for token in resp_obj['tokens']: 37 | print("Deleting {}: {}".format(token['canarytoken'], token['kind'])) 38 | data = { 39 | 'auth_token': auth, 40 | 'canarytoken': token['canarytoken'] 41 | } 42 | resp = requests.post(delete_url, data=data) 43 | print("-----------------------------------------------------------------------------------") 44 | print("All deleted. Go create some more! They're on us ;)") 45 | 46 | if __name__ == "__main__": 47 | main(sys.argv[1:]) 48 | -------------------------------------------------------------------------------- /alert_management.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #Constants 4 | DOMAIN="" 5 | AUTH_KEY="" 6 | SAVE=true 7 | DELETE=true 8 | FILEDATE=`date "+%Y%m%d%H%M%S"` 9 | 10 | FILENAME=$FILEDATE-$DOMAIN-export.json 11 | echo -e "\nThinkst Canary Alert Management" 12 | 13 | usage() 14 | { 15 | echo -e "\nBy default, this script will save, acknowledge and then delete your alerts" 16 | echo "Results are saved to the current working directory in JSON" 17 | echo -e "\t-d Domain of your Canary Console" 18 | echo -e "\t-a Auth Token for the Canary API" 19 | echo -e "\t-s Don't save the incidents (just acknowledge and delete)" 20 | echo -e "\t-r Don't remove the incidents (just acknowledge)" 21 | 22 | exit -1 23 | } 24 | 25 | while getopts "hd:a:rs" opt; do 26 | case $opt in 27 | h) 28 | usage 29 | ;; 30 | d) 31 | DOMAIN="${OPTARG}" 32 | ;; 33 | a) 34 | AUTH_KEY="${OPTARG}" 35 | ;; 36 | r) 37 | DELETE=false 38 | ;; 39 | s) 40 | SAVE=false 41 | ;; 42 | \?) 43 | echo -e "\nInvalid Option: -${OPTARG}" 1>&2 44 | usage 45 | exit -1 46 | ;; 47 | esac 48 | done 49 | shift $((OPTIND-1)) 50 | 51 | ping_console() { 52 | ping=$(curl -s "https://${DOMAIN}.canary.tools/api/v1/ping?auth_token=${AUTH_KEY}" | jq -r ".result") 53 | if [ ${ping} != "success" ]; then 54 | echo -e "\nConnection to the Console unsuccessful" 55 | exit -1 56 | fi 57 | echo -e "\nConnection to the Console successful" 58 | } 59 | 60 | save_incidents(){ 61 | echo -e "\nSaving the incidents" 62 | curl -s -X GET "https://${DOMAIN}.canary.tools/api/v1/incidents/unacknowledged?auth_token=${AUTH_KEY}" | jq '.incidents | .[]' >> ${FILENAME} 63 | echo "Incidents saved to ${PWD}/${FILENAME}" 64 | } 65 | 66 | acknowledge_incidents() { 67 | echo -e "\nThe following incidents have been acknowledged:" 68 | curl -s -X POST "https://${DOMAIN}.canary.tools/api/v1/incidents/acknowledge?auth_token=${AUTH_KEY}" | jq '.keys[]' 69 | } 70 | 71 | delete_incidents() { 72 | echo -e "\nThe following incidents have been deleted:" 73 | curl -s -X POST "https://${DOMAIN}.canary.tools/api/v1/incidents/delete?auth_token=${AUTH_KEY}" | jq '.keys[]' 74 | } 75 | ping_console 76 | 77 | if [ ${SAVE} == true ]; then 78 | save_incidents 79 | fi 80 | 81 | acknowledge_incidents 82 | 83 | if [ ${DELETE} == true ]; then 84 | delete_incidents 85 | fi 86 | -------------------------------------------------------------------------------- /CreateTokens.ps1: -------------------------------------------------------------------------------- 1 | # Script to create Canary tokens for a list of hosts. 2 | # We force TLS1.2 since our API doesn't support lower. 3 | [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12; 4 | Set-StrictMode -Version 2.0 5 | 6 | # Connect to API 7 | $ApiHost = [string]::Empty 8 | Do { 9 | $ApiHost = Read-Host -Prompt "Enter your Canary API domain" 10 | } Until (($ApiHost.Length -gt 0) -and ((Resolve-DnsName -DnsOnly -NoHostsFile -Name $ApiHost -Type A -ErrorAction SilentlyContinue)[0].IPAddress)) 11 | $ApiTokenSecure = New-Object System.Security.SecureString 12 | Do { 13 | $ApiTokenSecure = Read-Host -AsSecureString -Prompt "Enter your Canary API key" 14 | } Until ($ApiTokenSecure.Length -gt 0) 15 | $ApiToken = (New-Object System.Management.Automation.PSCredential "user",$ApiTokenSecure).GetNetworkCredential().Password 16 | $ApiBaseURL = '/api/v1' 17 | $PingResult = Invoke-RestMethod -Method Get -Uri "https://$ApiHost$ApiBaseURL/ping?auth_token=$ApiToken" 18 | $Result = $PingResult.result 19 | If ($Result -ne 'success') { 20 | Write-Host "Cannot ping Canary API. Bad token?" 21 | Exit 22 | } Else { 23 | Write-Host "Canary API available for service!" 24 | } 25 | 26 | $Targets = ( 27 | 'HOST1', 28 | 'HOST2', 29 | 'HOST3') 30 | 31 | ForEach ($TargetHostname in $Targets) { 32 | 33 | # Check whether token already exists 34 | $OutputFileName = "$TargetHostname-MSWORD.docx" 35 | If (Test-Path $OutputFileName) { 36 | Write-Host Skipping $TargetHostname, file already exists. 37 | Continue 38 | } 39 | 40 | # Create token 41 | $TokenName = "$TargetHostname-MSWORD" 42 | $PostData = @{ 43 | auth_token = "$ApiToken" 44 | kind = "doc-msword" 45 | memo = "$TokenName" 46 | } 47 | $CreateResult = Invoke-RestMethod -Method Post -Uri "https://$ApiHost$ApiBaseURL/canarytoken/create" -Body $PostData 48 | $Result = $CreateResult.result 49 | If ($Result -ne 'success') { 50 | Write-Host "Creation of $TokenName failed." 51 | Exit 52 | } Else { 53 | $WordTokenID = $($CreateResult).canarytoken.canarytoken 54 | Write-Host "$TokenName created (ID: $WordTokenID)." 55 | } 56 | 57 | # Download token 58 | Invoke-RestMethod -Method Get -Uri "https://$ApiHost$ApiBaseURL/canarytoken/download?auth_token=$ApiToken&canarytoken=$WordTokenID" -OutFile "$OutputFileName" 59 | } 60 | -------------------------------------------------------------------------------- /canary_api2csv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Intended to get basic alert information into a SIEM. Rather than pulling full 4 | # alert information, this script pulls just enough data to correlate Canary and 5 | # Canarytoken alerts with other events or to trigger the IR process. 6 | # 7 | # Initially, this script will pull the last week's worth of information. The 8 | # script keeps track of events exported and only pulls in new alerts. To 9 | # reset this script, simply delete the last.txt file in the same directory as 10 | # this script. 11 | 12 | # TODO: do a console ping that shows usage if ping fails 13 | # implement error checking for curl/jq commands 14 | # don't output a file if no data 15 | 16 | # Requires curl and jq to be in the path 17 | # sudo apt install curl jq 18 | 19 | # Set this variable to your API token (grab it here: https://1234abcd.canary.tools/settings where "1234abcd" is your unique console's CNAME) 20 | export token=deadbeef12345678 21 | 22 | # Customize this variable to match your console URL 23 | export console=1234abcd.canary.tools 24 | 25 | # Do a console ping - if it fails, print usage (not yet implemented) 26 | 27 | # Date format (one week ago) 28 | export weekago=`date --date="1 week ago" "+%Y-%m-%d-%H:%M:%S"` 29 | 30 | # Date format (current date) 31 | export currdate=`date "+%Y-%m-%d-%H:%M:%S"` 32 | 33 | # Filename date (current date, diff format that's file friendly) 34 | export filedate=`date "+%Y%m%d%H%M%S"` 35 | 36 | # Complete Filename 37 | export filename=$filedate-$console-alerts.csv 38 | 39 | # Base URL 40 | export baseurl="https://$console/api/v1/incidents/all?auth_token=$token&shrink=true&newer_than" 41 | 42 | # Run the jewels 43 | echo Datetime,Alert Description,Target,Target Port,Attacker,Attacker RevDNS > $filename 44 | # Check for previous runs 45 | if [ -f "last.txt" ]; then 46 | export lastdate=`cat last.txt` 47 | echo Last run was on $lastdate, grabbing everything since then. 48 | curl -s "$baseurl=$lastdate" | jq -r '.incidents[] | [.description | .created_std, .description, .dst_host, .dst_port, .src_host, .src_host_reverse | tostring] | @csv' >> $filename 49 | echo $currdate > last.txt 50 | echo Results saved in $filename. 51 | else 52 | # If no previous runs, do first run 53 | echo First run, grabbing the last week of alerts. 54 | curl -s "$baseurl=$weekago" | jq -r '.incidents[] | [.description | .created_std, .description, .dst_host, .dst_port, .src_host, .src_host_reverse | tostring] | @csv' >> $filename 55 | echo $currdate > last.txt 56 | echo Results saved in $filename 57 | fi 58 | -------------------------------------------------------------------------------- /DeployTokens.ps1: -------------------------------------------------------------------------------- 1 | # This script is designed to run as a startup script 2 | 3 | Set-StrictMode -Version 2.0 4 | 5 | # Source directory containing generated host-specific tokens. Tokens should be named HOSTNAME-MSWORD.docx. 6 | $TokenSourcePath = '\\fileserver.local.domain\Path\TokensByHost' 7 | 8 | # Path where you want the token to be deployed. 9 | $TargetPath = 'C:\Users\Administrator' 10 | $TargetPathSubdir = 'Desktop' 11 | $ACLFlag = "$TargetPath\ACLfixed" 12 | 13 | # Desired file name of deployed token. 14 | $TokenFilename = 'new azure password.docx' 15 | 16 | # Create target directory if it does not exists 17 | If (!(Test-Path $TargetPath)) { 18 | New-Item -ItemType Directory -Force -Path $TargetPath | Out-Null 19 | } 20 | If (!(Test-Path $TargetPath\$TargetPathSubdir)) { 21 | New-Item -ItemType Directory -Force -Path "$TargetPath\$TargetPathSubdir" | Out-Null 22 | } 23 | 24 | # Set required permissions 25 | If (!(Test-Path $ACLFlag)) { 26 | $SystemUser = New-Object System.Security.Principal.NTAccount('NT AUTHORITY', 'SYSTEM') 27 | $HomedirACL = Get-Acl -Path "$TargetPath" 28 | # Set owner 29 | $HomedirACL.SetOwner($SystemUser) 30 | # Disable inheritance without copying current perissions 31 | $HomedirACL.SetAccessRuleProtection($true, $false) 32 | # Set permissions (see: https://stackoverflow.com/questions/3282656/setting-inheritance-and-propagation-flags-with-set-acl-and-powershell) 33 | $HomedirACL.SetAccessRule($(New-Object System.Security.AccessControl.FileSystemAccessRule('NT AUTHORITY\SYSTEM', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))) 34 | $HomedirACL.SetAccessRule($(New-Object System.Security.AccessControl.FileSystemAccessRule('BUILTIN\Administrators', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))) 35 | $HomedirACL.SetAccessRule($(New-Object System.Security.AccessControl.FileSystemAccessRule('ASITIS\Administrator', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))) 36 | $HomedirACL.SetAccessRule($(New-Object System.Security.AccessControl.FileSystemAccessRule('NT AUTHORITY\Everyone', 'ReadAndExecute', 'ContainerInherit', 'None', 'Allow'))) 37 | Set-Acl -Path "$TargetPath" -AclObject $HomedirACL 38 | $SubdirACL = Get-Acl -Path "$TargetPath\$TargetPathSubdir" 39 | $SubdirACL.AddAccessRule($(New-Object System.Security.AccessControl.FileSystemAccessRule('NT AUTHORITY\Everyone', 'ReadAndExecute', 'ObjectInherit', 'None', 'Allow'))) 40 | Set-Acl -Path "$TargetPath\$TargetPathSubdir" -AclObject $SubdirACL 41 | New-Item -ItemType File -Force -Path $ACLFlag | Out-Null 42 | } 43 | 44 | # Deploy token 45 | $ThisHostname = "$($env:COMPUTERNAME)" 46 | If (Test-Path "$TokenSourcePath\$ThisHostname-MSWORD.docx") { 47 | Copy-Item -Path "$TokenSourcePath\$ThisHostname-MSWORD.docx" -Destination "$TargetPath\$TargetPathSubdir\$TokenFilename" -Exclude (Get-ChildItem "$TargetPath\$TargetPathSubdir") 48 | } 49 | -------------------------------------------------------------------------------- /deploy_tokens.ps1: -------------------------------------------------------------------------------- 1 | $Scriptblock ={ 2 | param($Server) 3 | $CanaryHost = 'https://xxxxxxxx.canary.tools' 4 | $CanaryApiKey = '' 5 | $FlockId='flock:default' 6 | 7 | $ApiPing = "$CanaryHost/api/v1/ping?auth_token=$CanaryApiKey" 8 | $ApiCreateToken = "$CanaryHost/api/v1/canarytoken/create?auth_token=$CanaryApiKey" 9 | $ApiDownloadToken = "$CanaryHost/api/v1/canarytoken/download?auth_token=$CanaryApiKey" 10 | 11 | $Memo =([String]'Password file stored on '+$Server.name) 12 | $OutputFile = ([String]'C:\Users\Administrator\Passwords_'+$Server.name+'.docx') 13 | 14 | # Process script parameters 15 | if ($Memo -eq $null -or $OutputFile -eq $null) { 16 | Write-Host -ForegroundColor Red "Please supply a Memo and an OutputFile path" 17 | return 18 | } 19 | 20 | # Setup error handling by clearing all previous errors 21 | $error.Clear() 22 | $ErrorActionPreference = 'SilentlyContinue' 23 | 24 | # Check that the API is reachable (both host and API key are valid) 25 | Invoke-WebRequest -Uri "$ApiPing" | out-null 26 | if ($error.count -ne 0) { 27 | Write-Host -ForegroundColor Red "Could not connect to the Canary Console at $CanaryHost." 28 | Write-Host -ForegroundColor White -BackgroundColor Black "The error was:" 29 | Write-Host $error[0] 30 | Write-Host -ForegroundColor White -BackgroundColor Black "Please ensure the Canary Console hostname and API key are present in the script." 31 | return 32 | } 33 | 34 | # Create the Canarytoken on the Canary Console 35 | $postParams = @{kind='doc-msword';memo="$Memo";flock_id=$FlockId} 36 | $tokenString = Invoke-WebRequest -Uri "$ApiCreateToken" -Method POST -Body $postParams 37 | if ($error.count -ne 0) { 38 | Write-Host -ForegroundColor Red "Could not create Canarytoken." 39 | Write-Host -ForegroundColor White -BackgroundColor Black "The error was:" 40 | Write-Host $error[0] 41 | return 42 | } 43 | # Extract the Canarytoken value from the response 44 | $tokenData = ConvertFrom-Json -InputObject $tokenString 45 | $canarytoken = $tokenData.canarytoken.canarytoken 46 | 47 | # Download the MS Word file associated with the newly created Canarytoken 48 | # and output to the file .docx 49 | try { 50 | Invoke-WebRequest -Uri "$ApiDownloadToken&canarytoken=$canarytoken" -OutFile "$OutputFile" 51 | Unblock-File -Path "$OutputFile" 52 | } catch { 53 | if(($_.Exception.GetType() -match "HttpResponseException") -and 54 | ($_.Exception -match "302")) { 55 | $downloadFileUrl = $_.Exception.Response.Headers.Location.AbsoluteUri 56 | $error.Clear() 57 | Invoke-Webrequest -Uri $downloadFileUrl -OutFile "$OutputFile" 58 | Unblock-File -Path "$OutputFile" 59 | } else { 60 | throw $_ 61 | } 62 | } 63 | 64 | if ($error.count -ne 0) { 65 | Write-Host -ForegroundColor Red "Could not download the tokened document." 66 | Write-Host -ForegroundColor White -BackgroundColor Black "If you contact support@canary.tools, please include this output:" 67 | Get-Host | Select-Object Name,Version 68 | Write-Host "Error:" 69 | Write-Host $error[0] 70 | } 71 | $b = New-PSSession -ComputerName $Server.dnshostname 72 | Copy-Item -ToSession $b $OutputFile -Destination C:\Users\Administrator\Desktop\password.docx 73 | $b | Remove-PSSession 74 | Write-Host 'Canary Token (password.docx) written to'$srv_name 75 | } 76 | 77 | $RunspacePool = [runspacefactory]::CreateRunspacePool(1,10) 78 | $RunspacePool.Open() 79 | $Jobs = @() 80 | 81 | Get-ADComputer -Filter * -Properties * | Select name, dnshostname | Foreach-Object { 82 | $PowerShell = [Powershell]::Create() 83 | $Powershell.RunspacePool = $RunspacePool 84 | $Powershell.AddScript($Scriptblock).AddArgument($_) 85 | $Jobs += $PowerShell.BeginInvoke() 86 | } 87 | while ($Jobs.IsCompleted -contains $False) {Start-Sleep -Milliseconds 100} 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Thinkst Canary Scripts and Resources 2 | A hodgepodge of humble but helpful scripts created for Thinkst Canary customers. 3 | 4 | While it's great that most products and services these days have APIs, they're often oversold. The answer to any question about a missing feature can be, "you can do anything with our product, it has an API!" 5 | 6 | Logically, your next thought might be, "sure, but that API would be a lot more useful if I had a few spare Python developers to throw at a few projects..." 7 | 8 | In this spirit, we often build scripts and bits of code to help customers automate and integrate things withour API. In some cases, our fantastic customers even write the code and donate it back to us. 9 | 10 | Happy birding! 11 | 12 | ## Script Descriptions and Usage 13 | In general, most of these scripts will need to be edited to add your Canary Console URL (in the form of ab1234ef.canary.tools) and your API key, which can be found in the Canary Console settings. 14 | 15 | ### CreateTokens.ps1 16 | **Author:** This Powershell script was kindly donated by a Thinkst customer 17 | **Purpose:** Create Microsoft Word document tokens for a list of target systems. (one DOCX token per host) 18 | **Usage:** This script doesn't require any arguments. However, you'll need to manually edit the script to add a list of hosts (starting on line 26). You'll also need to edit it if you want to use a different token type. 19 | 20 | In the future, we'll likely update this script to take a list of hosts from an external command (e.g. net view /domain) or from an external text file. Perhaps we can also extend it in the future to output different types of tokens as well. 21 | 22 | ### DeployTokens.ps1 23 | **Author:** This Powershell script was kindly donated by a Thinkst customer 24 | **Purpose:** This script is intended to deploy the tokens created by CreateTokens.ps1 25 | **Usage:** As with CreateTokens.ps1, no arguments are taken with this script, you'll need to manually edit it to point it at the tokens you've created and to change the destination for the token. By default, it gets placed in c:\Users\Administrator\Desktop 26 | 27 | ### alert_management.sh 28 | **Author:** Thinkst (Matt) 29 | **Purpose** This script is a quick and easy way to export alert data out of the console and clean up the alerts all at once. 30 | **Usage:** Run this script with the -h flag to read the usage. API details can be entered at runtime, or edited into the script directly. Additional options exist to save and acknowledge (don't delete) or to acknowledge and delete (don't save). 31 | 32 | ### canary_alert_extract.sh 33 | **Author:** Thinkst (Adrian) 34 | **Purpose:** This shell script came from a customer request to dump alerts to a spreadsheet-friendly format 35 | **Usage:** As with the Powershell scripts, using this script requires a bit of manual editing. Customize the API token and Canary Console variables and the shell script can be run with no arguments to produce a CSV containing the last week's alerts. 36 | 37 | ### canary_api2csv.sh 38 | **Author:** Thinkst (Adrian) 39 | **Purpose:** Intended for SIEM use - only pulls unique new alerts that haven't been pulled previously and exports them to a CSV file. Suitable for a cron job that runs this command and places files in a location where the SIEM knows to pick them up and ingest them. 40 | **Usage:** Edit the file to copy in your unique console URL and API key. Then, just run the script with no arguments. 41 | 42 | ### canaryconsole.py 43 | **Author:** Thinkst (Adrian) 44 | **Purpose:** This is a commandline version of the Canary console. Functionality is limited to read-only functions at this stage, but it may be further developed into a tool that makes it easier to deploy large numbers of Canarytokens or make mass changes to Canaries. 45 | **Usage:** Type ```python3 canaryconsole.py``` and it will do the rest, including prompting for console name and API key. 46 | 47 | ### canarygen_awscreds.cmd 48 | **Author:** Thinkst (Adrian) 49 | **Purpose:** This is a Windows version of the following python script. It's designed to generate one unique AWS credentials token per host. 50 | **Usage:** The script needs to be edited to set the Console and API key variables. Requires [JQ](https://stedolan.github.io/jq/) and Curl to either be in the path, or for the path to be customized in the script. 51 | 52 | ### canarygen_awscreds_auto.py 53 | **Author:** Thinkst (Adrian) 54 | **Purpose:** This python script generates unique AWS credential tokens each time it is run. This script is designed to run once per host, as the description for each token is customized using local environment variables (username and hostname). 55 | **Usage:** This is the 'auto' version of this script (the 'arguments' version isn't finished yet), meaning that you'll have to manually edit the script to set your Console and API key variables. 56 | 57 | ### canarygen_awscreds_auto.sh 58 | **Author:** Thinkst (Adrian) 59 | **Purpose:** This shell script generates unique AWS credential tokens each time it is run. It was specifically designed to run with zero dependencies (as opposed to the python version of this script, which has a few). It is designed to run once per host, as the description for each token is customized using local environment variables (username and hostname). 60 | **Usage:** This is the 'auto' version of this script (the 'arguments' version isn't finished yet), meaning that you'll have to manually edit the script to set your Console and API key variables. 61 | **Compatibility:** This script has been tested and confirmed to run correctly on macOS (Catalina and High Sierra) and Ubuntu 18.04. 62 | 63 | ### delete_tokens.py 64 | **Author:** Thinkst (Jay) 65 | **Purpose:** This script came from a customer that was testing creating large amounts of tokens. They needed a quick way to 'clean up' their console while testing, so we built this script (with many disclaimers!) to wipe a console clean of Canarytokens. 66 | **Usage:** `python3 delete_tokens.py ` 67 | 68 | ### deploy_tokens.ps1 69 | **Author:** Thinkst (Bradley) 70 | **Purpose:** A sample for mass deploying tokens in parallel across Active Directory. 71 | **Usage:** `deploy_tokens.ps1` 72 | 73 | ### yellow - just add blue 74 | **Author:** Dominic White (singe) 75 | **Purpose:** A simple binary wrapper that will trigger a Canarytoken when a binary is executed. 76 | **Link to Repo:** [singe/yellow](https://github.com/singe/yellow) 77 | -------------------------------------------------------------------------------- /canaryconsole.py: -------------------------------------------------------------------------------- 1 | # CanaryCLI 0.2 2 | 3 | # Prereqs include: 4 | # pip3 install canarytools console-menu PTable 5 | import canarytools 6 | import json 7 | import requests 8 | import os 9 | import os.path 10 | import time 11 | from os import path 12 | from consolemenu import * 13 | from consolemenu.items import * 14 | from prettytable import PrettyTable 15 | 16 | # Prep for canarytools calls and raw API calls 17 | if not "TARCC_CONSOLE" in os.environ: 18 | consolehex = input("\nYou don't appear to have a Canary Console configured. \nPlease enter just your eight-digit CNAME - the part that comes before 'canary.tools'.\nIt should look something like 'ab123456': ") 19 | os.environ["TARCC_CONSOLE"] = consolehex 20 | consoleperm = input("Would you like to set this console permanently, so that you don't have to reset this value on future sessions? \n[Y/N]: ") 21 | if consoleperm.upper() == "Y": 22 | #print("Chose yes...") 23 | home = os.path.expanduser("~") 24 | #print("Home is " + home) 25 | #time.sleep(2) 26 | if path.exists(home + "/.bashrc"): 27 | print("\nFound a .bashrc, going to write to it") 28 | profile = open(home + "/.bashrc", "a") 29 | profile.write("\nexport TARCC_CONSOLE=" + consolehex) 30 | profile.close() 31 | print("Writing done, closing .bashrc") 32 | time.sleep(2) 33 | if path.exists(home + "/.zshrc"): 34 | print("\nFound a .zshrc, going to write to it") 35 | profile = open(home + "/.zshrc", "a") 36 | profile.write("\nexport TARCC_CONSOLE=" + consolehex) 37 | profile.close() 38 | print("Writing done, closing .zshrc") 39 | time.sleep(2) 40 | else: 41 | consolehex = os.getenv("TARCC_CONSOLE") 42 | 43 | if not "TARCC_APIKEY" in os.environ: 44 | auth = input("\nYou don't appear to have an API KEY set for your Canary Console. \nEnsure API is enabled in your Canary Console settings. \nThen copy the key and paste here: ") 45 | os.environ["TARCC_APIKEY"] = auth 46 | consoleperm = input("Would you like to set this API KEY permanently, so that you don't have to reset this value on future sessions? \n[Y/N]: ") 47 | if consoleperm.upper() == "Y": 48 | home = os.path.expanduser("~") 49 | if path.exists(home + "/.bashrc"): 50 | print("\nFound a .zshrc, going to write to it") 51 | profile = open(home + "/.bashrc", "a") 52 | profile.write("\nexport TARCC_APIKEY=" + auth) 53 | profile.close() 54 | print("Writing done, closing .bashrc") 55 | time.sleep(2) 56 | if path.exists(home + "/.zshrc"): 57 | print("\nFound a .zshrc, going to write to it") 58 | profile = open(home + "/.zshrc", "a") 59 | profile.write("\nexport TARCC_APIKEY=" + auth) 60 | profile.close() 61 | print("Writing done, closing .zshrc") 62 | time.sleep(2) 63 | else: 64 | auth = os.getenv("TARCC_APIKEY") 65 | 66 | consoleurl = consolehex + ".canary.tools" 67 | console = canarytools.Console(consolehex, auth) 68 | 69 | # Ping the console to make sure our console URL and API keys are legit 70 | def check_api(): 71 | ping_apiurl = "https://{base}/api/v1/ping?auth_token={auth}".format(base=consoleurl, auth=auth) 72 | ping_console = requests.get(ping_apiurl) 73 | ping_results = ping_console.json() 74 | if ping_results["result"] != "success": 75 | print("\nERROR - Please check your:\nCanary Console URL ({}) \nAPI key ({}).\n\nThey didn't work for us.".format(consoleurl, auth)) 76 | Screen().input('\nPress [Enter] to continue') 77 | exit(0) 78 | 79 | # Pull the console name to display 80 | def consolename(): 81 | settings_apiurl = "https://{base}/api/v1/settings?auth_token={auth}".format(base=consoleurl, auth=auth) 82 | pull_settings = requests.get(settings_apiurl) 83 | settings = pull_settings.json() 84 | console_name = settings["console_domain"] 85 | return console_name 86 | 87 | # Pull licensing details 88 | def license_usage(): 89 | license_apiurl = "https://{base}/api/v1/license?auth_token={auth}".format(base=consoleurl, auth=auth) 90 | pull_licenses = requests.get(license_apiurl) 91 | licenses = pull_licenses.json() 92 | 93 | # Build a table. No inserting tabs or spaces to get columns to line up anymore! 94 | license_table = PrettyTable() 95 | license_table.field_names = ["Type", "Total", "Used"] 96 | license_table.add_row(["Physical", licenses["devices"]["total"], licenses["devices"]["used"]]) 97 | license_table.add_row(["Virtual", licenses["vm"]["total"], licenses["vm"]["used"]]) 98 | license_table.add_row(["Cloud", licenses["cloud"]["total"], licenses["cloud"]["used"]]) 99 | print(license_table) 100 | 101 | Screen().input('\nPress [Enter] to continue') 102 | 103 | # Device list functions 104 | def livebirds_details(): 105 | 106 | livebirds_table = PrettyTable() 107 | livebirds_table.field_names = ["Name", "Location", "IP", "Uptime"] 108 | for device in console.devices.live(): 109 | livebirds_table.add_row([device.name, device.location, device.ip_address, device.uptime_age]) 110 | livebirds_table.sortby = "Name" 111 | print(livebirds_table) 112 | 113 | Screen().input('\nPress [Enter] to continue') 114 | 115 | def deadbirds_details(): 116 | 117 | deadbirds_table = PrettyTable() 118 | deadbirds_table.field_names = ["Name", "Location", "Personality", "Downtime"] 119 | for device in console.devices.dead(): 120 | deadbirds_table.add_row([device.name, device.location, device.ippers, device.uptime_age]) 121 | deadbirds_table.sortby = "Name" 122 | print(deadbirds_table) 123 | 124 | Screen().input('\nPress [Enter] to continue') 125 | 126 | # Alert handling functions 127 | def unackalerts(): 128 | unackalertnum = len(console.incidents.unacknowledged()) 129 | unackalerts_table = PrettyTable() 130 | unackalerts_table.field_names = ["Type", "Attacker"] 131 | for alert in console.incidents.unacknowledged(): 132 | alertcount = 0 133 | unackalerts_table.add_row([alert.summary, alert.src_host]) 134 | if alertcount == unackalertnum: 135 | exit(0) 136 | print(unackalerts_table) 137 | 138 | Screen().input('\nPress [Enter] to continue') 139 | 140 | def ackalerts(): 141 | ackalertnum = len(console.incidents.acknowledged()) 142 | ackalerts_table = PrettyTable() 143 | ackalerts_table.field_names = ["Type", "Attacker"] 144 | for alert in console.incidents.acknowledged(): 145 | alertcount = 0 146 | ackalerts_table.add_row([alert.summary, alert.src_host]) 147 | if alertcount == ackalertnum: 148 | exit(0) 149 | print(ackalerts_table) 150 | 151 | Screen().input('\nPress [Enter] to continue') 152 | 153 | # Token list function 154 | def canarytoken_listing(): 155 | tokennum = len(console.tokens.all()) 156 | tokencount = 0 157 | screennum = 0 158 | token_table = PrettyTable() 159 | token_table.field_names = ["Kind", "Memo"] 160 | for token in console.tokens.all(): 161 | token_table.add_row([token.kind, token.memo]) 162 | tokencount = tokencount + 1 163 | if tokencount == 10: 164 | print(token_table) 165 | currtotalmax = screennum * 10 + tokencount 166 | currtotalmin = currtotalmax - 9 167 | input("Showing tokens {} through {} of {} \n\nPress [Enter] to continue".format(currtotalmin, currtotalmax, tokennum)) 168 | token_table.clear_rows() 169 | tokencount = 0 170 | screennum = screennum + 1 171 | Screen().clear() 172 | elif screennum * 10 + tokencount == tokennum: 173 | print(token_table) 174 | currtotalmax = screennum * 10 + tokencount 175 | currtotalmin = screennum * 10 176 | input("Showing tokens {} through {} of {} \n\nPress [Enter] to return to the menu".format(currtotalmin, currtotalmax, tokennum)) 177 | 178 | # Build the stats screen 179 | def statscreen(): 180 | # Unacknowledged alert count 181 | unackalerts = len(console.incidents.unacknowledged()) 182 | # Acknowledged alert count 183 | ackalerts = len(console.incidents.acknowledged()) 184 | # Online Canary Count 185 | livebirds = len(console.devices.live()) 186 | # Offline Canary Count 187 | deadbirds = len(console.devices.dead()) 188 | # Tokens count 189 | tokencount = len(console.tokens.all()) 190 | 191 | print("Canary Status - Live: {} Dead: {}".format(livebirds, deadbirds)) 192 | print("Token Count - {}".format(tokencount)) 193 | print("Incident Status - Acked: {} Unacked: {}".format(ackalerts, unackalerts)) 194 | Screen().input('\nPress [Enter] to continue') 195 | 196 | def inyoni(): 197 | # Print Inyoni on exit 198 | print(" _\n _| '>\n / )\n / / /\n /_/ /\n /- \\ \\\n + +\n") 199 | 200 | # Build the menu 201 | def main(): 202 | check_api() 203 | console_name = consolename() 204 | # Splash Screen with basic stats, followed by menu 205 | menu = ConsoleMenu("Lo-fi Canary Console Main Menu", "CLI Edition, version 0.1", epilogue_text="Current console: " + console_name + " (" + consoleurl + ")") 206 | switch_consoles = MenuItem("Switch Consoles", menu) 207 | 208 | # Build the Devicelist Submenu 209 | device_submenu = ConsoleMenu("Canary Details", "Choose an item below") 210 | device_submenu_livebirds = FunctionItem("Live Bird Details", livebirds_details) 211 | device_submenu_deadbirds = FunctionItem("Dead Bird Details", deadbirds_details) 212 | device_submenu.append_item(device_submenu_livebirds) 213 | device_submenu.append_item(device_submenu_deadbirds) 214 | 215 | # Attach the Devicelist Submenu to the Main menu 216 | devices_item = SubmenuItem("Canary Details", submenu=device_submenu) 217 | devices_item.set_menu(menu) 218 | 219 | # Build the Canarytokens Submenu 220 | canarytokens_submenu = ConsoleMenu("Canarytokens", "Choose an item below", epilogue_text="eb603878.canary.tools") 221 | canarytokens_submenu_tokenlist = FunctionItem("Token Details", canarytoken_listing) 222 | canarytokens_submenu_createtokens = MenuItem("Create Canarytokens (not yet implemented)") 223 | canarytokens_submenu.append_item(canarytokens_submenu_tokenlist) 224 | canarytokens_submenu.append_item(canarytokens_submenu_createtokens) 225 | 226 | # Attach the Canarytokens Submenu to the Main menu 227 | canarytokens_item = SubmenuItem("Canarytokens", submenu=canarytokens_submenu) 228 | canarytokens_item.set_menu(menu) 229 | 230 | # Build the Incidents Submenu 231 | incidents_submenu = ConsoleMenu("Alerts", "Choose an item below", epilogue_text="eb603878.canary.tools") 232 | incidents_submenu_unackalerts = FunctionItem("Unacknowledged Alerts", unackalerts) 233 | incidents_submenu_ackalerts = FunctionItem("Acknowledged Alerts", ackalerts) 234 | incidents_submenu.append_item(incidents_submenu_unackalerts) 235 | incidents_submenu.append_item(incidents_submenu_ackalerts) 236 | 237 | # Attach the incidents Submenu to the Main menu 238 | incidents_item = SubmenuItem("Alerts", submenu=incidents_submenu) 239 | incidents_item.set_menu(menu) 240 | 241 | # Add all items to the root menu 242 | menu.append_item(FunctionItem("Current Stats", statscreen)) 243 | menu.append_item(devices_item) 244 | menu.append_item(canarytokens_item) 245 | menu.append_item(incidents_item) 246 | menu.append_item(FunctionItem("License Usage", license_usage)) 247 | menu.append_item(switch_consoles) 248 | 249 | # Show the menu 250 | menu.start() 251 | menu.join() 252 | 253 | if __name__ == "__main__": 254 | main() --------------------------------------------------------------------------------