情報セキュリティ関連業務にはログ分析が付き物であるものの、当該ログが最初からログ基盤に入っているとはかぎりません。システム担当者からログを圧縮ファイルとして受け取ることも多々あります。
受け取ったログを直接Rに取り込んで分析することも可能ですが、最初に大雑把に状況を把握するために、私はSplunkを使っています。 Splunkで情報付与を行なったり要調査フィールドを絞り込んだのち、詳細を調べたいときにRで調査しています。
SplunkへはGUIでログファイルを取り込めます。ただ、1ファイルずつですし、最大500MBという容量制限があり、使い勝手がよくありません。他方、日常のログ転送には Heavy Forwarderを設定するわけですが、1回かぎりの使用には大げさですよね。
そこで、HEC(HTTP Event Collector)の利用を考えます。
今回は、AWSのログをSplunkに取り込むことにします。具体的には、 Apache Combined形式に似たS3アクセスログと JSON形式であるCluodTrail(AWSのAPI操作ログ)とを対象にします。 AWSのドキュメントにサンプルが掲載されているので、そちらを流用しましょう。
以下は、S3アクセスログのサンプルです。あとで利用するために、変数に格納してあります。
s3access_smpl <- r"(
79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be DOC-SHOGUNEXAMPLE-BUCKET1 [06/Feb/2019:00:00:38 +0000] 192.0.2.3 79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be 3E57427F3EXAMPLE REST.GET.VERSIONING - "GET /DOC-EXAMPLE-BUCKET1?versioning HTTP/1.1" 200 - 113 - 7 - "-" "S3Console/0.4" - s9lzHYrFp76ZVxRcpX9+5cjAnEH2ROuNkd2BHfIa6UkFVdtjf5mKR3/eTPFvsiP/XV/VLi31234= SigV4 ECDHE-RSA-AES128-GCM-SHA256 AuthHeader DOC-EXAMPLE-BUCKET1.s3.us-west-1.amazonaws.com TLSV1.2 arn:aws:s3:us-west-1:123456789012:accesspoint/example-AP Yes
)"
次に、CloudTrailログのサンプルです。(本当は整形されておらず、1行になっています。)
cloudtrail_smpl <- r"(
{"Records": [{
"eventVersion": "1.08",
"userIdentity": {
"type": "IAMUser",
"principalId": "AIDA6ON6E4XEGITEXAMPLE",
"arn": "arn:aws:iam::888888888888:user/Mary",
"accountId": "888888888888",
"accessKeyId": "AKIAIOSFODNN7EXAMPLE",
"userName": "Mary",
"sessionContext": {
"sessionIssuer": {},
"webIdFederationData": {},
"attributes": {
"creationDate": "2023-07-19T21:11:57Z",
"mfaAuthenticated": "false"
}
}
},
"eventTime": "2023-07-19T21:25:09Z",
"eventSource": "iam.amazonaws.com",
"eventName": "CreateUser",
"awsRegion": "us-east-1",
"sourceIPAddress": "192.0.2.0",
"userAgent": "aws-cli/2.13.5 Python/3.11.4 Linux/4.14.255-314-253.539.amzn2.x86_64 exec-env/CloudShell exe/x86_64.amzn.2 prompt/off command/iam.create-user",
"requestParameters": {
"userName": "Richard"
},
"responseElements": {
"user": {
"path": "/",
"arn": "arn:aws:iam::888888888888:user/Richard",
"userId": "AIDA6ON6E4XEP7EXAMPLE",
"createDate": "Jul 19, 2023 9:25:09 PM",
"userName": "Richard"
}
},
"requestID": "2d528c76-329e-410b-9516-EXAMPLE565dc",
"eventID": "ba0801a1-87ec-4d26-be87-EXAMPLE75bbb",
"readOnly": false,
"eventType": "AwsApiCall",
"managementEvent": true,
"recipientAccountId": "888888888888",
"eventCategory": "Management",
"tlsDetails": {
"tlsVersion": "TLSv1.2",
"cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
"clientProvidedHostHeader": "iam.amazonaws.com"
},
"sessionCredentialFromConsole": "true"
}]}
)"
Splunkはすでに導入しているとします。ログのフィールド名を正規化するために、 Splunk Add-on for Amazon Web Servicesを入れておきましょう。
HECを有効化して、HECトークンを発行します。このあたりの画面操作は、
にて丁寧に記載されているので、そちらをご参照ください。
私は、MSTICPyの記事で使用したのと同じように、 VMware Workstation ProにインストールしたDocker版Splunkで準備しました。
です。
取得したHECトークンをkeyringにセットしておきます。
pacman::p_load(tidyverse, httr2, reticulate, jsonlite, keyring)
key_set_with_value("splunk-photon",
username = "awslog",
password = "Splunk 69d81181-467b-4618-957b-c2804bb7xxxx")
RでWeb APIを操作するには、httr2パッケージが便利です。「ヒッター2」と呼びます。
まずはコードの流れを確認しましょう。見ての通り、リクエストに関連する関数名は一貫して「req」で始まるようになっており、パイプの使用に最適化されています。
req <- request("https://splunk-photon:8088/") # ①
req |>
req_options(ssl_verifyhost = FALSE) |>
req_options(ssl_verifypeer = FALSE) |> # ②
req_url_path_append("services") |>
req_url_path_append("collector") |>
req_url_path_append("raw") |> # ③
req_url_query(index = "main") |>
req_url_query(sourcetype = "aws:s3:accesslogs") |>
req_url_query(channel = uuid::UUIDgenerate()) |> # ④
req_headers(Accept = "application/json") |>
req_headers(Authorization = key_get("splunk-photon", "awslog")) |> # ⑤
req_body_raw(s3access_smpl) |> # ⑥
req_dry_run() # ⑦
request()関数で、Splunk
HECのFQDNとポート番号とを指定します。デフォルトのポート番号は8088です。(Splunk
Cloudでは443。)
req_options()はlibcurlの任意のオプションを設定するための関数です。手元のSplunkはオレオレ証明書で動いているので、証明書の検証を行わないようにしています。curl
-kオプションと同じです。
req_url_append()で、URLのパスを組み立てています。結果として、
https:///splunk-photon:8088/services/collector/rawという
APIエンドポイントを指定しています。このエンドポイントについては、 HECのマニュアルを参照してください。
req_url_query()でURLのクエリパラメーターを指定しています。クエリ名(キー)には引用符は必要ありません。
さて、indexやsourcetypeには馴染みがあるでしょうが、 channelというクエリパラメーターはHEC独自のもので、資源調整のために導入されているとのことです。
The concept of a channel was introduced in HEC primarily to prevent a fast client from impeding the performance of a slow client. When you assign one channel per client, because channels are treated equally on Splunk Enterprise, one client can’t affect another.
channelにはGUID(UUID)を指定しますので、uuid::UUIDgenerate()関数でランダムな
UUIDを生成させています。
req_headers()は名前の通りリクエストヘッダーを設定するもので、ここで
HECトークンを指定しています。
req_body_raw()で、文字列(S3アクセスログの1行分)をボディに指定しています。ほかの例としては、req_body_json()があります。引数にRのリストを指定すると、
JSONに変換してボディを作成します。
req_dry_run()は寸止め関数で、組み立てたリクエストを文字列として表示させるだけで、実行はしません。
これで問題なさそうなので、リクエストを実行します。req_dry_run()のかわりにreq_perform()を用い、レスポンスをrespに代入しています。
req <- request("https://splunk-photon:8088/")
resp <-
req |>
req_options(ssl_verifyhost = FALSE) |>
req_options(ssl_verifypeer = FALSE) |>
req_url_path_append("services") |>
req_url_path_append("collector") |>
req_url_path_append("raw") |>
req_url_query(index = "main") |>
req_url_query(sourcetype = "aws:s3:accesslogs") |>
req_url_query(channel = uuid::UUIDgenerate()) |>
req_headers(Accept = "application/json") |>
req_headers(Authorization = key_get("splunk-photon", "awslog")) |>
req_body_raw(s3access_smpl) |>
req_perform()
ひとまずステータスコードを確認しましょう。
resp$status_code
## [1] 200
レスポンスボディはJSONで返ってきます。resp_body_json()で閲覧できます。このように、レスポンスに関する関数は一貫してrespで始まります。
resp |> resp_body_json()
## $text
## [1] "Success"
##
## $code
## [1] 0
正常に登録できたようなので、Splunkから検索してみましょう。 MSTICPyを使います。
use_python("~/home/mambaforge/envs/msticpy/python.exe")
mp <- import("msticpy")
prov_splunk <- mp$QueryProvider("Splunk")
prov_splunk$connect(host="splunk-photon", port="8089",
username="admin", password=keyring::key_get("splunk-photon", "admin"))
spl_sample_s3 <- r"(
search
index="main"
sourcetype="aws:s3:accesslogs"
timeformat="%F %T%z"
earliest="2019-02-06 00:00:00+0000"
latest="2019-02-06 00:01:00+0000"
| eval dttm=strftime(_time,"%F %T%z")
| table dttm, action, operation, bucket_name, src, dest, http_user_agent
)"
df_sample_s3 <- prov_splunk$exec_query(spl_sample_s3, timeout=300)
Pandasのデータフレームも表示可能ですが、いちおうTibbleに変換します。
df_sample_s3 |> as_tibble()
| dttm | action | operation | bucket_name | src | dest | http_user_agent |
|---|---|---|---|---|---|---|
| 2019-02-06 00:00:38+0000 | OK | REST.GET.VERSIONING | DOC-EXAMPLE-BUCKET1 | 192.0.2.3 | DOC-EXAMPLE-BUCKET1.s3.us-west-1.amazonaws.com | S3Console/0.4 |
うまくいきました。
注目すべきは、アクセス日時が_timeフィールドとして自動的に選定されている点です。これは、props.confでそのような設定がなされているからです。
_timeフィールドの抽出がうまくいかないときには、インデックス作成日時が
_timeになります。
ちなみに、私がSplunkを使うときには、timeformatをISO形式(+タイムゾーンあり)で指定することを習慣づけています。デフォルトの米式表記は年月日を変更するのに不便ですし、タイムゾーンをシステム依存にしたくはないからです。
同様にして、CloudTrailのログを取り込んでみました。ところが、結果が芳しくありません。このアドオンではRecords[]が存在していると、フィールドの正規化に失敗します。
CLIで行うなら、jqを用いてRecords[]を除去すればよいでしょう。たとえば
$ cat cloudtrail_sample.json | jq .Records[]
のようにします。
別の方法として、以前にも紹介したSOF-ELKに含まれるaws-cloudtrail2sof-elk.pyを使う手もあります。SOF-ELKはパケットやフロー、ログの分析が容易にできるように、ログファイルを置いておくだけでElasticに取り込んでくれる機能を有しています。そこで用いられているスクリプトを流用するのです。
上記のGitHubのサイトからPythonスクリプトをダウンロードして、たとえば
$ python3 aws-cloudtrail2sof-elk.py -r /path/to/log -w ./combined.json -f
とすれば、/path/to/log配下のCloudTrailログが再帰的に取り込まれ(gzにも対応)、
1本のJSONファイルとして出力されます。実務的には、この方法が手軽だと思います。
以下では弥縫策としてjsonliteパッケージの関数群を使っていますが、効率のよい方法ではなく、お薦めできません。
cloudtrail_smpl_fixed <-
minify(cloudtrail_smpl) |>
fromJSON(simplifyDataFrame = FALSE) |>
pluck(1) |>
toJSON(auto_unbox = TRUE) |>
str_sub(2L, -2L)
cloudtrail_smpl_fixed
## [1] "{\"eventVersion\":\"1.08\",\"userIdentity\":{\"type\":\"IAMUser\",\"principalId\":\"AIDA6ON6E4XEGITEXAMPLE\",\"arn\":\"arn:aws:iam::888888888888:user/Mary\",\"accountId\":\"888888888888\",\"accessKeyId\":\"AKIAIOSFODNN7EXAMPLE\",\"userName\":\"Mary\",\"sessionContext\":{\"sessionIssuer\":{},\"webIdFederationData\":{},\"attributes\":{\"creationDate\":\"2023-07-19T21:11:57Z\",\"mfaAuthenticated\":\"false\"}}},\"eventTime\":\"2023-07-19T21:25:09Z\",\"eventSource\":\"iam.amazonaws.com\",\"eventName\":\"CreateUser\",\"awsRegion\":\"us-east-1\",\"sourceIPAddress\":\"192.0.2.0\",\"userAgent\":\"aws-cli/2.13.5 Python/3.11.4 Linux/4.14.255-314-253.539.amzn2.x86_64 exec-env/CloudShell exe/x86_64.amzn.2 prompt/off command/iam.create-user\",\"requestParameters\":{\"userName\":\"Richard\"},\"responseElements\":{\"user\":{\"path\":\"/\",\"arn\":\"arn:aws:iam::888888888888:user/Richard\",\"userId\":\"AIDA6ON6E4XEP7EXAMPLE\",\"createDate\":\"Jul 19, 2023 9:25:09 PM\",\"userName\":\"Richard\"}},\"requestID\":\"2d528c76-329e-410b-9516-EXAMPLE565dc\",\"eventID\":\"ba0801a1-87ec-4d26-be87-EXAMPLE75bbb\",\"readOnly\":false,\"eventType\":\"AwsApiCall\",\"managementEvent\":true,\"recipientAccountId\":\"888888888888\",\"eventCategory\":\"Management\",\"tlsDetails\":{\"tlsVersion\":\"TLSv1.2\",\"cipherSuite\":\"ECDHE-RSA-AES128-GCM-SHA256\",\"clientProvidedHostHeader\":\"iam.amazonaws.com\"},\"sessionCredentialFromConsole\":\"true\"}"
では、実行してみます。
req <- request("https://splunk-photon:8088/")
resp <-
req |>
req_options(ssl_verifyhost = FALSE) |>
req_options(ssl_verifypeer = FALSE) |>
req_url_path_append("services") |>
req_url_path_append("collector") |>
req_url_path_append("raw") |>
req_url_query(index = "main") |>
req_url_query(sourcetype = "aws:cloudtrail") |>
req_url_query(channel = uuid::UUIDgenerate()) |>
req_headers(Accept = "application/json") |>
req_headers(Authorization = key_get("splunk-photon", "awslog")) |>
req_body_raw(cloudtrail_smpl_fixed) |>
req_perform()
正しくフィールド抽出ができました。
1行のログ転送に成功したので、今度はファイルを送ることを考えます。とはいっても、/services/collector/rawエンドポイントはボディに複数行の文字列を入れると1行1イベントと解釈してパースするので、特に変えるべきところはありません。
以下に、簡単な関数を作っています。
send_tosplunk <- function(host, port, token, index, sourcetype, logfile, dry_run = TRUE){
pacman::p_load(tidyverse, httr2)
channel <- uuid::UUIDgenerate()
message <- read_file(logfile)
req <- request(str_glue("https://{host}:{port}/"))
req_mod <-
req |>
req_options(ssl_verifyhost = FALSE) |>
req_options(ssl_verifypeer = FALSE) |>
req_url_path_append("services") |>
req_url_path_append("collector") |>
req_url_path_append("raw") |>
req_url_query(index = index) |>
req_url_query(sourcetype = sourcetype) |>
req_url_query(channel = channel) |>
req_headers(Accept = "application/json") |>
req_headers(Authorization = token) |>
req_body_raw(message)
if(dry_run == TRUE){
req_mod |> req_dry_run()
} else {
req_mod |> req_perform()
}
}
次のようにして使います。
send_tosplunk(host = "splunk-photon",
port = 8088,
token = key_get("splunk-photon", "awslog"),
index = "main",
sourcetype = "aws:cloudtrail",
logfile = "data/combined.json",
dry_run = FALSE)
私の手元では、3万行程度(47MB)のCloudTrailのログは一瞬で取り込めました。ギガバイト級のファイルをアップロードするなら、5万行ずつリクエストを区切るなどの手法を考えるべきかもしれません。
Splunk Cloudでは、HECのURLとポートとがEnterprise版とは異なります。
https://http-inputs-<host>.splunkcloud.com/<endpoint>
今回は、httr2パッケージを用いてSplunkにログを転送しました。 httr2は、Rate Limitへの対応などWeb APIへのアクセスに有用な関数が整備されています。httrをお使いの方も、移行をご検討ください。
ここまでは1行1レコードとなる/services/collector/rawエンドポイントを使ってきました。しかしSplunk
HECには、もう1つ/services/collector/eventというエンドポイントがあります。
こちらのエンドポイントを使う場合には、イベントの各レコードをJSONのeventの中にJSONオブジェトクとして書いてやる必要があります。また、indexやsource、sourcetypeも同様にJSONの中に書きます。
先ほどのCloudTrailのサンプルを取り上げましょう。
event_template <- r"({
"index": "main",
"source": "dfirblog",
"sourcetype": "aws:cloudtrail",
"event": __EVENT__
})"
body_event <-
event_template |>
str_replace("__EVENT__", cloudtrail_smpl_fixed)
このリクエストは、以下の通りです。auto_extract_timestampというクエリパラメーターに留意してください。これをtrueにしていないと、
_timeを設定してくれません。
req <- request("https://splunk-photon:8088/")
resp <-
req |>
req_options(ssl_verifyhost = FALSE) |>
req_options(ssl_verifypeer = FALSE) |>
req_url_path_append("services") |>
req_url_path_append("collector") |>
req_url_path_append("event") |>
req_url_query(channel = uuid::UUIDgenerate()) |>
req_url_query(auto_extract_timestamp = "true") |>
req_headers(Accept = "application/json") |>
req_headers(Authorization = key_get("splunk-photon", "awslog")) |>
req_body_raw(body_event) |>
req_dry_run()
## POST /services/collector/event?channel=2277e6a1-3259-4d13-a3d1-3b00df44c5f5&auto_extract_timestamp=true HTTP/1.1
## Host: splunk-photon:8088
## User-Agent: httr2/1.0.0 r-curl/5.2.0 libcurl/8.3.0
## Accept-Encoding: deflate, gzip
## Accept: application/json
## Authorization: Splunk 69d81181-467b-4618-957b-c2804bb703e7
## Content-Length: 1382
##
## {
## "index": "main",
## "source": "dfirblog",
## "sourcetype": "aws:cloudtrail",
## "event": {"eventVersion":"1.08","userIdentity":{"type":"IAMUser","principalId":"AIDA6ON6E4XEGITEXAMPLE","arn":"arn:aws:iam::888888888888:user/Mary","accountId":"888888888888","accessKeyId":"AKIAIOSFODNN7EXAMPLE","userName":"Mary","sessionContext":{"sessionIssuer":{},"webIdFederationData":{},"attributes":{"creationDate":"2023-07-19T21:11:57Z","mfaAuthenticated":"false"}}},"eventTime":"2023-07-19T21:25:09Z","eventSource":"iam.amazonaws.com","eventName":"CreateUser","awsRegion":"us-east-1","sourceIPAddress":"192.0.2.0","userAgent":"aws-cli/2.13.5 Python/3.11.4 Linux/4.14.255-314-253.539.amzn2.x86_64 exec-env/CloudShell exe/x86_64.amzn.2 prompt/off command/iam.create-user","requestParameters":{"userName":"Richard"},"responseElements":{"user":{"path":"/","arn":"arn:aws:iam::888888888888:user/Richard","userId":"AIDA6ON6E4XEP7EXAMPLE","createDate":"Jul 19, 2023 9:25:09 PM","userName":"Richard"}},"requestID":"2d528c76-329e-410b-9516-EXAMPLE565dc","eventID":"ba0801a1-87ec-4d26-be87-EXAMPLE75bbb","readOnly":false,"eventType":"AwsApiCall","managementEvent":true,"recipientAccountId":"888888888888","eventCategory":"Management","tlsDetails":{"tlsVersion":"TLSv1.2","cipherSuite":"ECDHE-RSA-AES128-GCM-SHA256","clientProvidedHostHeader":"iam.amazonaws.com"},"sessionCredentialFromConsole":"true"}
## }
また、1リクエストで複数のイベントを登録するときには、
{"event": "record 1"}
{"event": "record 2"}
のようにします。
こうしてみると、/services/collector/rawエンドポイントのほうが簡単そうです。ただし/services/collector/eventには、timestampを自分で設定できるという利点があります。
このことが意味を持つのは、syslogのように不十分な日時データしか持たないログです。たとえば以下のイベントは、年もタイムゾーンも分かりません。
Mar 27 13:06:56 ip-10-77-20-248 sshd[1291]: Server listening on 0.0.0.0 port 22.
これをデフォルトの状態でSplunkに取り込むと、直近の2023年3月27日となり、タイムゾーンはシステムに依存します。このログの取得時が2021年でタイムゾーンが JSTなのであれば、下のようにエポックを計算して、JSONのtimeフィールドに設定します。
log_raw <- "Mar 27 13:06:56 ip-10-77-20-248 sshd[1291]: Server listening on 0.0.0.0 port 22."
ptn <- r"{^(?<time>[^ ]* {1,2}[^ ]* [^ ]*)}"
event_time <-
log_raw |>
str_match(ptn) |>
pluck(1) |>
parse_date_time2("b d H:M:S", tz = "Asia/Tokyo") |>
update(year = 2021) |>
as.integer() |>
as.character()
event_templates <- r"({
"index": "main",
"sourcetype": "linux_secure",
"time": __TIMESTAMP__
"event": __EVENT__
})"
syslog_smpl <-
event_templates |>
str_replace("__TIMESTAMP__", event_time) |>
str_replace("__EVENT__", log_raw)
syslog_smpl
## [1] "{\n \"index\": \"main\",\n \"sourcetype\": \"linux_secure\",\n \"time\": 1616818016\n \"event\": Mar 27 13:06:56 ip-10-77-20-248 sshd[1291]: Server listening on 0.0.0.0 port 22.\n}"
下の画像で、上のイベントはtimeを指定しなかったもの、下のイベントが timeを2021年かつJSTとしてエポック秒を指定したものです。
/services/collector/rawと/services/collector/eventとの違いの詳細をお知りになりたい方は、
Vinicius Egerland「Splunk
HTTP Event Collectors Explained」をご一読ください。
以下のコードは、send_tosplunk()関数をPowerShellにしたものです。証明書の検証を迂回させるために、少し面倒なことをしています。(PowerShell 7であればコマンドレットにパラメーターが存在している。)
<#
.SYNOPSIS
Sends log files to Splunk using the HTTP Event Collector.
.DESCRIPTION
This function sends a log file to Splunk's HTTP Event Collector using specified parameters.
.PARAMETER Computer
The hostname of the Splunk HEC. This parameter is mandatory.
.PARAMETER Port
The port number for the Splunk HEC. This parameter is mandatory.
.PARAMETER Token
The authentication token for Splunk HEC. This parameter is mandatory.
.PARAMETER Index
The index in Splunk to which the data is sent. This parameter is mandatory.
.PARAMETER Sourcetype
The sourcetype in Splunk for the data. This parameter is mandatory.
.PARAMETER Logfile
The path to the log file that will be sent. This parameter is mandatory.
.PARAMETER DryRun
Set to $True to perform a dry run without actual sending, $False to send the data.
This parameter is optional and defaults to $True.
.EXAMPLE
Send-ToSplunk -Computer "host.example.com" -Port 8088 -Token "yourToken" -Index "yourIndex" -Sourcetype "yourSourcetype" -Logfile "path\to\logfile.log" -DryRun $False
Sends the specified log file to Splunk using the provided parameters.
#>
function Send-ToSplunk {
param(
[Parameter(Mandatory=$true)]
[string]$Computer,
[Parameter(Mandatory=$true)]
[int]$Port,
[Parameter(Mandatory=$true)]
[string]$Token,
[Parameter(Mandatory=$true)]
[string]$Index,
[Parameter(Mandatory=$true)]
[string]$Sourcetype,
[Parameter(Mandatory=$true)]
[string]$Logfile,
[bool]$DryRun = $true
)
$channel = [guid]::NewGuid().ToString()
$message = Get-Content -Path $Logfile
$queryParams = "index=${Index}&sourcetype=${Sourcetype}"
$uri = "https://${Computer}:${Port}/services/collector/raw?${queryParams}"
$headers = @{
"Authorization" = "Splunk ${Token}"
"X-Splunk-Request-Channel" = $channel
}
Add-Type @"
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustAllCertsPolicy : ICertificatePolicy {
public bool CheckValidationResult(
ServicePoint srvPoint, X509Certificate certificate,
WebRequest request, int certificateProblem) {
return true;
}
}
"@
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
if ($DryRun) {
Write-Host "Dry run: Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -InFile $Logfile -UseBasicParsing"
} else {
Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -InFile $Logfile -UseBasicParsing
}
}