# Windowsでのリモートアクション用スクリプトの作成

{% hint style="info" %}
この記事に記載された操作に関する支援が必要な場合は、Nexthink認定パートナーにご連絡ください。
{% endhint %}

この記事では、Windows上でNexthinkリモートアクションスクリプトを準備するプロセスの詳細について説明します。 スクリプトは、Windows .NET Framework上に構築されたMicrosoftのスクリプト言語であるPowerShellで作成され、その後、安全性を確保するために証明書で署名されます。 PowerShellスクリプトは、タスクの自動化や構成管理に適しており、従業員デバイス上でリモートアクションを実行することができます。

{% hint style="warning" %}
デバイスが制約付き言語モードでPowerShellを実行している場合、リモートアクションはサポートされていません。 このモードでは、多くの基本機能がブロックされるため、ほとんどのアクションが期待通りに実行されません。

Nexthinkは、リモートアクションをフル言語モードで実行することを推奨します。
{% endhint %}

リモートアクションの主な利用ケースには、デバイスからのオンデマンドデータ収集、自己修復タスク、構成設定の変更が含まれます。

この記事は、読者がPowerShellスクリプト作成に精通していることを前提としています。

{% hint style="info" %}
スクリプトを安全に実行する方法については、Nexthinkセキュリティの[Remote Actionsのセキュリティベストプラクティス](https://docs.nexthink.com/security/security-best-practices/remote-actions-security-best-practices)を参照してください。
{% endhint %}

## スクリプトの作成

### 汎用スクリプトと入力変数

汎用スクリプトは、署名済みスクリプトにカスタマイズが必要な状況で役立ちます。 署名済みスクリプトを変更すると署名が無効になりますが、汎用スクリプトはパラメーターを通してカスタマイズでき、署名を保持します。

PowerShellスクリプトの冒頭で形式的なパラメーターを宣言し、それを汎用化します。 関連するリモートアクションを編集するとき、パラメーターの値はNexthinkのwebインターフェース内で変更できます。

たとえば、汎用的なレジストリキーから読み込むスクリプトを作成するには、レジストリ内のキーへのパスを含むパラメーターをスクリプトで宣言します。 複数のリモートアクションがスクリプト内のパラメーターに異なるパスを指定することにより、異なるレジストリキーからデータを読み取るために同じスクリプトを使用できます。

```
param(
    [string]$filePath,
    [string]$regPath
)
```

リモートアクションの構成中にスクリプトをアップロードすると、システムは任意のインポートされたPowerShellスクリプトのパラメーターを認識し、**パラメーター**セクションに一覧表示します。 各パラメーター名の右側に表示されるテキスト入力ボックスに実際の値を入力します。

{% hint style="info" %}
実際の値は常にテキストとしてスクリプトに渡されます：スクリプトが**string**以外のタイプのパラメーターを宣言している場合、スクリプトが期待するタイプに変換できる値を提供するようにしてください。
{% endhint %}

### 出力変数の作成

スクリプトを実行すると、オンデマンドデータとして保存したい出力が生成されることがあります。 Nexthinkは、Collectorと同時に従業員のデバイスにインストールされる.NETアセンブリ（`nxtremoteactions.dll`）を提供します。 このアセンブリには、結果をデータ層に書き込むためのメソッドを提供するNxtというクラスが含まれています。

Nxtクラスを使うには、リモートアクションのためのPowerShellスクリプトの冒頭に次の行を追加します：

```
Add-Type -Path $env:NEXTHINK\RemoteActions\nxtremoteactions.dll
```

Nxtクラスのメソッドを使って、望む出力を書き込みます。 すべての書き込みメソッドは2つの引数を受け入れます：出力の名前と書き込む値です。 例えば、ファイルサイズをデータ層に書き込むには：

```
[Nxt]::WriteOutputSize("FileSize", $fileSize)
```

リモートアクション設定中にスクリプトをアップロードすると、システムはスクリプト内での出力書き込みの呼び出しを認識し、スクリプトテキストの下の\*\* Outputs \*\*セクションに出力変数を一覧表示します。 調査やメトリックで参照する際の方法を示すように出力のラベルを設定します。

書き込まれた各メソッドの終了は出力の種類を示します。 使用可能なメソッドと書き込む値の対応するPowerShellのタイプを以下の表で見つけてください：

| Nxt 書き込みメソッド          | PowerShell タイプ | 制約条件                                                        |
| --------------------- | -------------- | ----------------------------------------------------------- |
| WriteOutputString     | \[string]      | 0 - 1024バイト（大きい場合は出力が切り捨てられます）                              |
| WriteOutputBool       | \[bool]        | true / false                                                |
| WriteOutputUInt32     | \[uint32]      | <ul><li>最小: 0</li><li>最大: 4 294 967 295</li></ul>           |
| WriteOutputFloat      | \[float]       | <ul><li>最小: -3.4E+38</li><li>最大: 3.4E+38</li></ul>          |
| WriteOutputSize       | \[float]       | <ul><li>最小: 0</li><li>最大: 3.4E+38</li></ul>                 |
| WriteOutputRatio      | \[float]       |                                                             |
| WriteOutputBitRate    | \[浮動小数点]       |                                                             |
| WriteOutputDateTime   | \[DateTime]    | DD.MM.YYYY\@HH:MM                                           |
| WriteOutputDuration   | \[TimeSpan]    | <ul><li>最小: 0 ms</li><li>最大: 49日</li><li>ミリ秒単位の精度</li></ul> |
| WriteOutputStringList | \[string\[]]   | Stringと同様                                                   |

### **出力フィールドの定義**

スクリプトを開発する際は、必ず以下を確認してください:

* **すべての出力フィールドの名前を事前に定義する:** これにより、実行中にスクリプトが出力テーブルの正しいフィールドにデータを格納することが保証されます。 フィールド名を事前に定義していないと、未知の出力スキーマによりスクリプトが失敗する可能性があります。
  * 出力フィールド名は常に`string`形式であるべきです。
  * 例: `[Nxt]::WriteOutputString('Output_Field_Name', $Output_Value)`
* **出力フィールドの数を定義する:** スクリプトは常に固定された出力フィールド数を指定する必要があります。 固定スキーマは予測可能な結果と互換性を保証します。
  * 動的フィールドを避けてください。 動的な出力構造は、プラットフォームに不一致や処理エラーを引き起こす可能性があります。
  * スクリプトを実行する際に出力フィールドを定義するためにループを使用しないでください。

### キャンペーンの実施

リモートアクションをキャンペーンと組み合わせて、社員が問題を自主的に解決できるようにします。 キャンペーンにより、問題の検出について社員に知らせ、その解決を案内できます。

デバイスを操作中の社員のデスクトップにキャンペーンを表示させるには：

* キャンペーンは**リモートアクション**トリガーを持ち、公開されている必要があります。
* リモートアクションのスクリプトは次のいずれかで実行できます：
  * アクションが特別な権限を必要としない場合、社員のコンテキストで。
  * アクションが管理特権を必要とする場合、ローカルシステムアカウントのコンテキストで。

### **キャンペーン識別子の取得**

リモートアクションからキャンペーンを実行するメソッドには、引数としてキャンペーン識別子を渡す必要があります。 キャンペーンのNQL ID（<し>推奨）およびキャンペーンUIDの両方を使用できます。

{% hint style="info" %}
NQL IDを識別子としてサポートするには、Collectorバージョン<し>23.5以降が必要です。
{% endhint %}

リモートアクションにキャンペーン識別子を渡すには、リモートアクションのスクリプトで必要なそれぞれのキャンペーンのパラメーターを宣言します。 リモートアクションを編集する際、実際の値としてNQL ID（<し>またはUID）をパラメーターに使用します。

キャンペーンのNQL IDまたはUIDを取得する方法の詳細は、[キャンペーンをトリガーする](https://docs.nexthink.com/platform/ja/user-guide/campaigns/campaigns-in-finder-classic/triggering-a-campaign-manually-with-finder-classic)ドキュメントを参照してください。

### **リモートアクションのスクリプトからキャンペーンを実行する**

キャンペーンとインタラクションするには、リモートアクションスクリプトは、Collectorと共に社員のデバイスにインストールされた.NETアセンブリ（<し>`nxtcampaignaction.dll`）をロードする必要があります。 このアセンブリには、キャンペーンの実行を制御し、社員の回答を取得するメソッドを提供する**Nxt.CampaignAction**クラスが含まれています。

アセンブリをロードするには、スクリプトの冒頭に次の行を追加します：

```
Add-Type -Path $env:NEXTHINK\RemoteActions\nxtcampaignaction.dll
```

**Nxt.CampaignAction**のメソッドでキャンペーンを制御します。

```
[nxt.campaignaction]::RunCampaign(string campaignUid)
```

`campaignUid`で識別されるキャンペーンを実行し、社員がキャンペーンに答え終えるまで待機します。 `campaignUid`引数には、UIDまたはNQL ID（<し>推奨）を含めることができます。 `NxTrayResp`型のオブジェクトで応答を返します。

```
[nxt.campaignaction]::(string campaignUid, int timeout)
```

`campaignUid`で識別されるキャンペーンを実行し、社員がキャンペーンに答え終わるか、指定された`timeout`（<し>秒単位）で終了するまで待機します。 `campaignUid`引数には、UIDまたはNQL ID（<し>推奨）を含めることができます。 `NxTrayResp`型のオブジェクトで応答を返します。

```
[nxt.campaignaction]::RunStandAloneCampaign(string campaignUid)
```

`campaignUid`で識別されるキャンペーンを実行します。 `campaignUid`引数には、NQL ID（<し>推奨）またはUIDを含めることができます。

```
string GetResponseStatus(NxTrayResp response)
```

`NxTrayResp`型の応答オブジェクトに基づいて、キャンペーンのステータスを反映した文字列を返すメソッドです。 ステータスの可能な値：

* **fully**：社員がキャンペーンの質問に完全に回答しました。
* **declined**：社員がキャンペーンへの参加を辞退しました。
* **postponed**：社員がキャンペーンへの参加に同意しました。
* **timeout**：システムが社員の回答が完了する前にキャンペーンをタイムアウトしました。
* **connectionfailed**：Collectorコンポーネント間の通信の技術的エラーにより、キャンペーン通知を制御するCollectorコンポーネントに接続できませんでした。
* **notificationfailed**：スクリプトがキャンペーンを正常に表示できませんでした。以下の理由のいずれか：
  * キャンペーンが存在しないか未発表のため、プラットフォームからキャンペーンの定義を取得できませんでした。
  * 別のキャンペーンがすでに社員に表示されています。
  * 非緊急対応キャンペーンは、Collectorの集中保護または「取り込み禁止」ルールのため表示できません。 詳細については、[キャンペーンの受信率の制限](https://docs.nexthink.com/platform/ja/user-guide/campaigns/managing-campaigns/creating-campaigns/limiting-the-reception-rate-of-campaigns)ドキュメントを参照してください。

```
string[] GetResponseAnswer(NxTrayResp response, string questionLabel)
```

`NxTrayResp`型の応答オブジェクトとキャンペーン内の質問を識別するラベルを指定すると、社員の回答を返すメソッドです。

* 単一回答の質問の場合、返された文字列配列には一つの要素しかありません。
* 複数回答の質問の場合、返される文字列配列には、社員が選択した回答の数だけ要素が含まれます。 任意の自由テキストは無視されます。
* 社員がキャンペーンに完全に回答していない場合、たとえばステータスが`fully`でない場合、返される文字列配列は空になります。 任意の自由記述テキストは無視されます。

{% hint style="danger" %}
セキュリティ上の理由から、セルフヘルプシナリオのためのリモートアクションは、複数回答または意見スケール質問のオプションの自由テキスト回答を無視します。 セルフヘルプシナリオで使用すべきキャンペーンにオプションの自由テキスト回答を含めることは役立ちません。
{% endhint %}

### スクリプトのエンコード

PowerShellスクリプトファイルは、バイトオーダー・マーク（<し>BOM）付きのUTF-8でエンコードされていなければなりません。 BOMはファイルの冒頭に存在する必要があるUnicode文字であり、そのUTF-8での表現は3バイトの16進シーケンス`EF BB BF`です。

各コード行はWindowsでは`CR+LF`という文字シーケンスで終わらなければなりません。

適切なエンコーディングを確保して、エラーやスクリプトの不具合を回避してください。

***

## コード例

<details>

<summary>キャンペーンの呼び出し</summary>

この例では、リモートアクションが基本的なcampaign呼び出しをIDで実行し、成功した場合にはステータスメッセージを、失敗した場合にはエラーメッセージを出力します。

```powershell
$result = [Nxt.CampaignAction]::RunCampaign($CampaignUid, $maxWaitTimeinSeconds)
$status = [Nxt.CampaignAction]::GetResponseStatus($result)
if ($status -eq "fully") {
        Write-Output "キャンペーンが成功しました"
} else {
        Write-Output "ステータスは $status です"
}
```

</details>

<details>

<summary>キャンペーンの応答へのアクセス</summary>

この例では、リモートアクションがキャンペーンの回答データを呼び出し、それを配列として出力します。 各回答は対応する番号付きオプションで表されます。 PowerShellは0からn-1のインデックスを使用していることに注意してください。

```powershell
# キャンペーン応答を取得する関数
function Get-CampaignResponse ([string]$CampaignId) {
    return [nxt.campaignaction]::RunCampaign($CampaignId, $CAMPAIGN_TIMEOUT)
}
# キャンペーンのステータスを取得する関数
function Get-CampaignResponseStatus ($Response) {
    return [nxt.campaignaction]::GetResponseStatus($Response)
}

# 応答の回答を取得する関数
function Get-CampaignResponseAnswer ($Response, [string]$QuestionName) {
    return [nxt.campaignaction]::GetResponseAnswer($Response, $QuestionName)[0]
}

# キャンペーン応答の取得
$campaignResponse = $null
$campaignResponse = Get-CampaignResponse -CampaignId $campaignId

# キャンペーンステータスの取得
$status = $null
$status = Get-CampaignResponseStatus -Response $campaignResponse
Write-Host "応答ステータスは $status です"

# 応答の回答の取得
$answersArray = $null
$answer = Get-CampaignResponseAnswer -Response $campaignResponse -QuestionName "Question1"
Write-Host "答えは $answer です"
```

</details>

<details>

<summary>タイムアウト付きでキャンペーンを実行</summary>

この例では、リモートアクションが指定した時間後にタイムアウトし終了するcampaignを秒を入力として実行します。

```powershell
# タイムアウト付きでキャンペーンを実行
# タイムアウトは秒単位 (100秒または00:01:40)

$campaignId = "#my_campaign_nql_id"
$timeout = 100

function Get-CampaignResponse ([string]$CampaignId) {
    return [nxt.campaignaction]::RunCampaign($CampaignId, $timeout)
}

function Get-CampaignResponseStatus ($Response) {
    return [nxt.campaignaction]::GetResponseStatus($Response)
}

$result = Get-CampaignResponse -CampaignId $campaignId -timeout $timeout
$status = Get-CampaignResponseStatus -Response $result
if ($status -eq "fully") {
        Write-Output "キャンペーンが成功しました"
} else {
        Write-Output "ステータスは $status です"
}
```

</details>

<details>

<summary>ノンブロッキングキャンペーンの実行</summary>

この例では、リモートアクションがユーザー入力を必要とせず、キャンペーンをトリガーした後に実行を続けます。 ユーザーはいつでもキャンペーンを終了することができます。 これはデータを取得するのではなく、主にユーザーに情報を提供するために使われます。

```powershell
##### 非ブロッキングキャンペーンの実行 #####

$mycampaignId = "#my_campaign_nql_id"
function Invoke-OperationCompletedCampaign ([string]$CampaignId) {
    [nxt.campaignaction]::RunStandAloneCampaign($CampaignId)
}

Invoke-OperationCompletedCampaign -CampaignId $mycampaignId
```

</details>

<details>

<summary>特定のキャンペーンレスポンスでアプリケーションを開く</summary>

この例では、リモートアクションスクリプトが、Collectorとデバイスにインストールされた.dllファイルをロードし、Collectorとリモートアクション実行の橋渡しとして機能します。 PowerShellスクリプトからCollectorにコマンドを発行し、`[Nxt.CampaignAction]`で始まる専門機能を使用できるようにします。

リモートアクションは`Nxt.CampaignAction]::RunCampaign`関数を使用してキャンペーンを実行し、キャンペーンIDと秒単位のタイムアウトを入力として受け取ります。 次に、ユーザーの応答または応答のない状態を収集し、そのデータを使用してステータスを決定します。 ユーザーが`yes`と答えた場合、リモートアクションがプロセスを開始します。この場合、Notepadを起動します。

```powershell
Add-Type -Path "$env:NEXTHINK\RemoteActions\nxtcampaignaction.dll"

$CampaignUid  = "<NQL ID of a single-answer campaign>"
$maxWaitTimeinSeconds = 60

$result = [Nxt.CampaignAction]::RunCampaign($CampaignUid, $maxWaitTimeinSeconds)
$status = [Nxt.CampaignAction]::GetResponseStatus($result)

if ($status -eq "fully") {
    $questionName = "question1"
    $choiceName =[Nxt.CampaignAction]::GetResponseAnswer($result, $questionName)
    if ($choiceName -eq "yes") {
        # ユーザーが確認しましたので、いくつかのアクションを実行します：
        Start-Process notepad.exe
    }
}
```

</details>

<details>

<summary>特定のアプリケーションがデバイス上にあるか確認する</summary>

この例では、リモートアクションが入力として提供したアプリケーション名がデバイス上に存在するかKanopyを使用して確認します：

```powershell
#
# 入力パラメータの定義
#
param(
    [Parameter(Mandatory = $true)][string]$application_name
)
# パラメータ定義の終了

$env:Path = 'C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\'

#
# 定数の定義
#
$ERROR_EXCEPTION_TYPE = @{Environment = '[Environment error]'
    Input = '[Input error]'
    Internal = '[Internal error]'
}
Set-Variable -Name 'ERROR_EXCEPTION_TYPE' -Option ReadOnly -Scope Script -Force

$LOCAL_SYSTEM_IDENTITY = 'S-1-5-18'
Set-Variable -Name 'LOCAL_SYSTEM_IDENTITY' -Option ReadOnly -Scope Script -Force

$REMOTE_ACTION_DLL_PATH = "$env:NEXTHINK\RemoteActions\nxtremoteactions.dll"
Set-Variable -Name 'REMOTE_ACTION_DLL_PATH' -Option ReadOnly -Scope Script -Force

$WINDOWS_VERSIONS = @{Windows7 = '6.1'
    Windows8 = '6.2'
    Windows81 = '6.3'
    Windows10 = '10.0'
    Windows11 = '10.0'
}
Set-Variable -Name 'WINDOWS_VERSIONS' -Option ReadOnly -Scope Script -Force



#
# Mainを呼び出す
#
function Invoke-Main ([hashtable]$InputParameters) {
    $exitCode = 0
    $appPresent = $false
    try {
        Add-NexthinkRemoteActionDLL
        Test-RunningAsLocalSystem
        Test-MinimumSupportedOSVersion -WindowsVersion 'Windows8'
        Test-InputParameter -InputParameters $InputParameters

        $appPresent = Invoke-CheckApplcationExistance -appName $InputParameters.application_name
    } catch {
        Write-StatusMessage -Message $_
        $exitCode = 1
    } finally {
        Update-EngineOutputVariables -applicationPresent $appPresent
    }

    return $exitCode
}

#
# テンプレート関数
#
function Add-NexthinkRemoteActionDLL {

    if (-not (Test-Path -Path $REMOTE_ACTION_DLL_PATH)) {
        throw "$($ERROR_EXCEPTION_TYPE.Environment) Nexthink Remote Action DLL not found. "
    }
    Add-Type -Path $REMOTE_ACTION_DLL_PATH
}

function Test-RunningAsLocalSystem {

    if (-not (Confirm-CurrentUserIsLocalSystem)) {
        throw "$($ERROR_EXCEPTION_TYPE.Environment) This script must be run as LocalSystem. "
    }
}

function Confirm-CurrentUserIsLocalSystem {

    $currentIdentity = Get-CurrentIdentity
    return $currentIdentity -eq $LOCAL_SYSTEM_IDENTITY
}

function Get-CurrentIdentity {

    return [security.principal.windowsidentity]::GetCurrent().User.ToString()
}

function Test-MinimumSupportedOSVersion ([string]$WindowsVersion, [switch]$SupportedWindowsServer) {
    $currentOSInfo = Get-OSVersionType
    $OSVersion = $currentOSInfo.Version -as [version]

    $supportedWindows = $WINDOWS_VERSIONS.$WindowsVersion -as [version]

    if (-not ($currentOSInfo)) {
        throw "$($ERROR_EXCEPTION_TYPE.Environment) This script could not return OS version. "
    }

    if ( $SupportedWindowsServer -eq $false -and $currentOSInfo.ProductType -ne 1) {
        throw "$($ERROR_EXCEPTION_TYPE.Environment) This script is not compatible with Windows Servers. "
    }

    if ( $OSVersion -lt $supportedWindows) {
        throw "$($ERROR_EXCEPTION_TYPE.Environment) This script is compatible with $WindowsVersion and later only. "
    }
}

function Get-OSVersionType {

    return Get-WindowsManagementData -Class Win32_OperatingSystem | Select-Object -Property Version,ProductType
}

function Get-WindowsManagementData ([string]$Class, [string]$Namespace = 'root/cimv2') {
    try {
        $query = [wmisearcher] "Select * from $Class"
        $query.Scope.Path = "$Namespace"
        $query.Get()
    } catch {
        throw "$($ERROR_EXCEPTION_TYPE.Environment) Error getting CIM/WMI information. Verify WinMgmt service status and WMI repository consistency. "
    }
}

function Write-StatusMessage ([psobject]$Message) {
    $exceptionMessage = $Message.ToString()

    if ($Message.InvocationInfo.ScriptLineNumber) {
        $version = Get-ScriptVersion
        if (-not [string]::IsNullOrEmpty($version)) {
            $scriptVersion = "Version: $version. "
        }

        $errorMessageLine = $scriptVersion + "Line '$($Message.InvocationInfo.ScriptLineNumber)': "
    }

    $host.ui.WriteErrorLine($errorMessageLine + $exceptionMessage)
}

function Get-ScriptVersion {

    $scriptContent = Get-Content $MyInvocation.ScriptName | Out-String
    if ($scriptContent -notmatch '<#[\r\n]{2}.SYNOPSIS[^\#\>]*(.NOTES[^\#\>]*)\#>') { return }

    $helpBlock = $Matches[1].Split([environment]::NewLine)

    foreach ($line in $helpBlock) {
        if ($line -match 'Version:') {
            return $line.Split(':')[1].Split('-')[0].Trim()
        }
    }
}

function Test-StringNullOrEmpty ([string]$ParamName, [string]$ParamValue) {
    if ([string]::IsNullOrEmpty((Format-StringValue -Value $ParamValue))) {
        throw "$($ERROR_EXCEPTION_TYPE.Input) '$ParamName' cannot be empty nor null. "
    }
}

function Format-StringValue ([string]$Value) {
    return $Value.Replace('"', '').Replace("'", '').Trim()
}

#
# 入力パラメータの検証
#
function Test-InputParameter ([hashtable]$InputParameters) {
    Test-StringNullOrEmpty `
        -ParamName 'application_name' `
        -ParamValue $InputParameters.application_name
}

#
# アプリケーション管理
#
function Invoke-CheckApplcationExistance ([string]$appName) {

    $installedApps = Get-CimInstance -Query "SELECT * FROM Win32_Product WHERE Name LIKE '%$appName%'"

    if ($installedApps) {
        return $true
    } else {
        return $false
    }
}

#
# Nexthink Output管理
#
function Update-EngineOutputVariables ([bool]$applicationPresent) {

        [nxt]::WriteOutputBool('application_present', $applicationPresent)
}

#
# メインスクリプトの流れ
#
[environment]::Exit((Invoke-Main -InputParameters $MyInvocation.BoundParameters))
```

</details>

<details>

<summary>特定のアプリケーションがデバイスに存在するか確認する: エラーハンドリング</summary>

この例では、リモートアクションがアプリケーションログパスとエラーコードを入力として使用し、指定されたエラーコードのログを解析します。 このコードが存在する場合、エラーメッセージを出力します：

```powershell
#
# 入力パラメータの定義
#
param(
    [Parameter(Mandatory = $true)][string]$application_log_path,
    [Parameter(Mandatory = $true)][string]$error_code
)
# パラメータ定義の終了

$env:Path = 'C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\'

#
# 定数の定義
#
$ERROR_EXCEPTION_TYPE = @{Environment = '[Environment error]'
    Input = '[Input error]'
    Internal = '[Internal error]'
}
Set-Variable -Name 'ERROR_EXCEPTION_TYPE' -Option ReadOnly -Scope Script -Force

$LOCAL_SYSTEM_IDENTITY = 'S-1-5-18'
Set-Variable -Name 'LOCAL_SYSTEM_IDENTITY' -Option ReadOnly -Scope Script -Force

$REMOTE_ACTION_DLL_PATH = "$env:NEXTHINK\RemoteActions\nxtremoteactions.dll"
Set-Variable -Name 'REMOTE_ACTION_DLL_PATH' -Option ReadOnly -Scope Script -Force

$WINDOWS_VERSIONS = @{Windows7 = '6.1'
    Windows8 = '6.2'
    Windows81 = '6.3'
    Windows10 = '10.0'
    Windows11 = '10.0'
}
Set-Variable -Name 'WINDOWS_VERSIONS' -Option ReadOnly -Scope Script -Force



#
# Invoke Main
#
function Invoke-Main ([hashtable]$InputParameters) {
    $exitCode = 0
    $outputs = @{
        'error_message' = "-"
        'error_found' = $false
    }
    try {
        Add-NexthinkRemoteActionDLL
        Test-RunningAsLocalSystem
        Test-MinimumSupportedOSVersion -WindowsVersion 'Windows8'
        Test-InputParameter -InputParameters $InputParameters

        $outputs = Invoke-CheckApplicationLogError -applicationLogPath $InputParameters.application_log_path -errorCode $InputParameters.error_code
    } catch {
        Write-StatusMessage -Message $_
        $exitCode = 1
    } finally {
        Update-EngineOutputVariables -OutputData $outputs
    }

    return $exitCode
}

#
# テンプレート関数
#
function Add-NexthinkRemoteActionDLL {

    if (-not (Test-Path -Path $REMOTE_ACTION_DLL_PATH)) {
        throw "$($ERROR_EXCEPTION_TYPE.Environment) Nexthink Remote Action DLL not found. "
    }
    Add-Type -Path $REMOTE_ACTION_DLL_PATH
}

function Test-RunningAsLocalSystem {

    if (-not (Confirm-CurrentUserIsLocalSystem)) {
        throw "$($ERROR_EXCEPTION_TYPE.Environment) This script must be run as LocalSystem. "
    }
}

function Confirm-CurrentUserIsLocalSystem {

    $currentIdentity = Get-CurrentIdentity
    return $currentIdentity -eq $LOCAL_SYSTEM_IDENTITY
}

function Get-CurrentIdentity {

    return [security.principal.windowsidentity]::GetCurrent().User.ToString()
}

function Test-MinimumSupportedOSVersion ([string]$WindowsVersion, [switch]$SupportedWindowsServer) {
    $currentOSInfo = Get-OSVersionType
    $OSVersion = $currentOSInfo.Version -as [version]

    $supportedWindows = $WINDOWS_VERSIONS.$WindowsVersion -as [version]

    if (-not ($currentOSInfo)) {
        throw "$($ERROR_EXCEPTION_TYPE.Environment) This script could not return OS version. "
    }

    if ( $SupportedWindowsServer -eq $false -and $currentOSInfo.ProductType -ne 1) {
        throw "$($ERROR_EXCEPTION_TYPE.Environment) This script is not compatible with Windows Servers. "
    }

    if ( $OSVersion -lt $supportedWindows) {
        throw "$($ERROR_EXCEPTION_TYPE.Environment) This script is compatible with $WindowsVersion and later only. "
    }
}

function Get-OSVersionType {

    return Get-WindowsManagementData -Class Win32_OperatingSystem | Select-Object -Property Version,ProductType
}

function Get-WindowsManagementData ([string]$Class, [string]$Namespace = 'root/cimv2') {
    try {
        $query = [wmisearcher] "Select * from $Class"
        $query.Scope.Path = "$Namespace"
        $query.Get()
    } catch {
        throw "$($ERROR_EXCEPTION_TYPE.Environment) Error getting CIM/WMI information. Verify WinMgmt service status and WMI repository consistency. "
    }
}

function Write-StatusMessage ([psobject]$Message) {
    $exceptionMessage = $Message.ToString()

    if ($Message.InvocationInfo.ScriptLineNumber) {
        $version = Get-ScriptVersion
        if (-not [string]::IsNullOrEmpty($version)) {
            $scriptVersion = "Version: $version. "
        }

        $errorMessageLine = $scriptVersion + "Line '$($Message.InvocationInfo.ScriptLineNumber)': "
    }

    $host.ui.WriteErrorLine($errorMessageLine + $exceptionMessage)
}

function Get-ScriptVersion {

    $scriptContent = Get-Content $MyInvocation.ScriptName | Out-String
    if ($scriptContent -notmatch '<#[\r\n]{2}.SYNOPSIS[^\#\>]*(.NOTES[^\#\>]*)\#>') { return }

    $helpBlock = $Matches[1].Split([environment]::NewLine)

    foreach ($line in $helpBlock) {
        if ($line -match 'Version:') {
            return $line.Split(':')[1].Split('-')[0].Trim()
        }
    }
}

function Test-StringNullOrEmpty ([string]$ParamName, [string]$ParamValue) {
    if ([string]::IsNullOrEmpty((Format-StringValue -Value $ParamValue))) {
        throw "$($ERROR_EXCEPTION_TYPE.Input) '$ParamName' cannot be empty nor null. "
    }
}

function Format-StringValue ([string]$Value) {
    return $Value.Replace('"', '').Replace("'", '').Trim()
}

function Test-ParamIsInteger ([string]$ParamName, [string]$ParamValue) {
    $intValue = $ParamValue -as [int]
    if ([string]::IsNullOrEmpty($ParamValue) -or $null -eq $intValue) {
        throw "$($ERROR_EXCEPTION_TYPE.Input) Error in parameter '$ParamName'. '$ParamValue' is not an integer. "
    }
}

#
# 入力パラメータの検証
#
function Test-InputParameter ([hashtable]$InputParameters) {
    Test-StringNullOrEmpty `
        -ParamName 'application_log_path' `
        -ParamValue $InputParameters.application_log_path 
    Test-ValidPath `
        -ParamName 'application_log_path' `
        -ParamValue $InputParameters.application_log_path
    Test-ParamIsInteger `
        -ParamName 'error_code' `
        -ParamValue $InputParameters.error_code
}

function Test-ValidPath ([string]$ParamName, [string]$ParamValue) {
    if (-not (Test-Path -Path $ParamValue)) {
        throw "$ParamName is not a valid path or is not accessible."
    }
}

#
# アプリケーション管理
#
function Invoke-CheckApplicationLogError ([string]$applicationLogPath, [string]$errorCode) {

    $returnValues = @{
        'error_message' = "-"
        'error_found' = $false
    }

    $errorMatches = Select-String -Path $applicationLogPath -Pattern $errorCode | Select-Object -First 1

    if ($errorMatches) {
        $errorLine =  $errorMatches.Line
        $errorMessage = $errorLine.Split(":")[1].Trim()
        $returnValues.error_message = $errorMessage
        $returnValues.error_found = $true
        
    } else {
        $returnValues.error_found = $false
    }

    return $returnValues
}

#
# Nexthink Output management
#
function Update-EngineOutputVariables ([hashtable]$outputData) {

        [nxt]::WriteOutputString('error_message', $outputData.error_message )
        [nxt]::WriteOutputBool('error_found', $outputData.error_found )
}

#
# メインスクリプトの流れ
#
[environment]::Exit((Invoke-Main -InputParameters $MyInvocation.BoundParameters))
```

</details>

<details>

<summary>特定のアプリケーションがデバイスに存在するか確認する: エラー修復</summary>

この例では、Remote ActionがCampaignsを使用して、ユーザーにアプリケーションがすでに実行中である場合は再起動が必要であることを知らせるか、実行中でない場合は起動します：

```powershell
#
# 入力パラメータの定義
#
param(
    [Parameter(Mandatory = $true)][string]$initial_camapign_id,
    [Parameter(Mandatory = $true)][string]$final_campaign_id,
    [Parameter(Mandatory = $true)][string]$inform_failure_campaign_id
)
# パラメータ定義の終了

$env:Path = 'C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\'

#
# 定数の定義
#
$CAMPAIGN_DLL_PATH = "$env:NEXTHINK\RemoteActions\nxtcampaignaction.dll"
Set-Variable -Name 'CAMPAIGN_DLL_PATH' -Option ReadOnly -Scope Script -Force

$ERROR_EXCEPTION_TYPE = @{Environment = '[Environment error]'
    Input = '[Input error]'
    Internal = '[Internal error]'
}
Set-Variable -Name 'ERROR_EXCEPTION_TYPE' -Option ReadOnly -Scope Script -Force

$LOCAL_SYSTEM_IDENTITY = 'S-1-5-18'
Set-Variable -Name 'LOCAL_SYSTEM_IDENTITY' -Option ReadOnly -Scope Script -Force

$NQL_ID_FORMAT_REGEX = "^[#]*([a-zA-Z0-9_]+_)*[a-zA-Z0-9_#]*$"
Set-Variable -Name 'NQL_ID_FORMAT_REGEX' -Option ReadOnly -Scope Script -Force

$REMOTE_ACTION_DLL_PATH = "$env:NEXTHINK\RemoteActions\nxtremoteactions.dll"
Set-Variable -Name 'REMOTE_ACTION_DLL_PATH' -Option ReadOnly -Scope Script -Force

$WINDOWS_VERSIONS = @{Windows7 = '6.1'
    Windows8 = '6.2'
    Windows81 = '6.3'
    Windows10 = '10.0'
    Windows11 = '10.0'
}
Set-Variable -Name 'WINDOWS_VERSIONS' -Option ReadOnly -Scope Script -Force

$KANOPY_APPLICATION_PROCESS_NAME = 'kanopyagent'
Set-Variable -Name 'KANOPY_SERVICE_NAME' -Option ReadOnly -Scope Script -Force

$KANOPY_APPLICATION_EXECUTABLE_PATH = 'C:\ProgramData\Kanopy\KanopyAgent\KanopyAgent.exe'
Set-Variable -Name 'KANOPY_APPLICATION_EXECUTABLE_PATH' -Option ReadOnly -Scope Script -Force

#
# Mainを呼び出す
#
function Invoke-Main ([hashtable]$InputParameters) {
    $exitCode = 0

    try {
        Add-NexthinkDLLs
        Test-RunningAsInteractiveUser
        Test-MinimumSupportedOSVersion -WindowsVersion 'Windows8'
        Test-InputParameter -InputParameters $InputParameters

        $outputs = Invoke-RemediationAction -InputParameters $InputParameters
    } catch {
        Write-StatusMessage -Message $_
        $exitCode = 1
    }

    return $exitCode
}

#
# テンプレート関数
#
function Add-NexthinkDLLs {

    if (-not (Test-Path -Path $REMOTE_ACTION_DLL_PATH)) {
        throw "$($ERROR_EXCEPTION_TYPE.Environment) Nexthink Remote Action DLL not found. "
    }
    if (-not (Test-Path -Path $CAMPAIGN_DLL_PATH)) {
        throw "$($ERROR_EXCEPTION_TYPE.Environment) Nexthink Campaign DLL not found. "
    }
    Add-Type -Path $REMOTE_ACTION_DLL_PATH
    Add-Type -Path $CAMPAIGN_DLL_PATH
}

function Test-RunningAsInteractiveUser {

    if (Confirm-CurrentUserIsLocalSystem) {
        throw "$($ERROR_EXCEPTION_TYPE.Environment) This script must be run as InteractiveUser. "
    }
}

function Confirm-CurrentUserIsLocalSystem {

    $currentIdentity = Get-CurrentIdentity
    return $currentIdentity -eq $LOCAL_SYSTEM_IDENTITY
}

function Get-CurrentIdentity {

    return [security.principal.windowsidentity]::GetCurrent().User.ToString()
}

function Test-MinimumSupportedOSVersion ([string]$WindowsVersion, [switch]$SupportedWindowsServer) {
    $currentOSInfo = Get-OSVersionType
    $OSVersion = $currentOSInfo.Version -as [version]

    $supportedWindows = $WINDOWS_VERSIONS.$WindowsVersion -as [version]

    if (-not ($currentOSInfo)) {
        throw "$($ERROR_EXCEPTION_TYPE.Environment) This script could not return OS version. "
    }

    if ( $SupportedWindowsServer -eq $false -and $currentOSInfo.ProductType -ne 1) {
        throw "$($ERROR_EXCEPTION_TYPE.Environment) This script is not compatible with Windows Servers. "
    }

    if ( $OSVersion -lt $supportedWindows) {
        throw "$($ERROR_EXCEPTION_TYPE.Environment) This script is compatible with $WindowsVersion and later only. "
    }
}

function Get-OSVersionType {

    return Get-WindowsManagementData -Class Win32_OperatingSystem | Select-Object -Property Version,ProductType
}

function Get-WindowsManagementData ([string]$Class, [string]$Namespace = 'root/cimv2') {
    try {
        $query = [wmisearcher] "Select * from $Class"
        $query.Scope.Path = "$Namespace"
        $query.Get()
    } catch {
        throw "$($ERROR_EXCEPTION_TYPE.Environment) Error getting CIM/WMI information. Verify WinMgmt service status and WMI repository consistency. "
    }
}

function Write-StatusMessage ([psobject]$Message) {
    $exceptionMessage = $Message.ToString()

    if ($Message.InvocationInfo.ScriptLineNumber) {
        $version = Get-ScriptVersion
        if (-not [string]::IsNullOrEmpty($version)) {
            $scriptVersion = "Version: $version. "
        }

        $errorMessageLine = $scriptVersion + "Line '$($Message.InvocationInfo.ScriptLineNumber)': "
    }

    $host.ui.WriteErrorLine($errorMessageLine + $exceptionMessage)
}

function Get-ScriptVersion {

    $scriptContent = Get-Content $MyInvocation.ScriptName | Out-String
    if ($scriptContent -notmatch '<#[\r\n]{2}.SYNOPSIS[^\#\>]*(.NOTES[^\#\>]*)\#>') { return }

    $helpBlock = $Matches[1].Split([environment]::NewLine)

    foreach ($line in $helpBlock) {
        if ($line -match 'Version:') {
            return $line.Split(':')[1].Split('-')[0].Trim()
        }
    }
}

function Test-CampaignID ([string]$ParamName, [string]$ParamValue) {
    if ([string]::IsNullOrEmpty($ParamValue)) {
        throw "$($ERROR_EXCEPTION_TYPE.Input) Error in parameter '$ParamName'. Value cannot be null or empty. "
    }

    if (-not ($ParamValue -as [guid]) -and ($ParamValue -notmatch $NQL_ID_FORMAT_REGEX)) {
        throw "$($ERROR_EXCEPTION_TYPE.Input) Error in parameter '$ParamName'. Only UID or NQL ID values are accepted. "
    }
}

function Write-NxtLog ([string]$Message, [object]$Object) {
    if (Test-PowerShellVersion -MinimumVersion 5) {
        $currentDate = Get-Date -Format 'yyyy/MM/dd hh:mm:ss'
        if ($Object) {
            $jsonObject = $Object | ConvertTo-Json -Compress -Depth 100
            Write-Information -MessageData "$currentDate - $Message $jsonObject"
        } else {
            Write-Information -MessageData "$currentDate - $Message"
        }
    }
}

function Test-PowerShellVersion ([int]$MinimumVersion) {
    if ((Get-Host).Version.Major -ge $MinimumVersion) {
        return $true
    }
}

function Get-CampaignResponseTimeout ([string]$CampaignId, [int]$CampaignTimeout) {
    return [nxt.campaignaction]::RunCampaign($CampaignId,$CampaignTimeout)
}

function Get-CampaignResponseStatus ($Response) {
    return [nxt.campaignaction]::GetResponseStatus($Response)
}

function Get-CampaignResponseAnswer ($Response, [string]$QuestionName) {
    return [nxt.campaignaction]::GetResponseAnswer($Response, $QuestionName)[0]
}

function Invoke-OperationCompletedCampaign ([string]$CampaignId) {
    [nxt.campaignaction]::RunStandAloneCampaign($CampaignId)
}

#
# 入力パラメータの検証
#
function Test-InputParameter ([hashtable]$InputParameters) {
    Test-CampaignID `
        -ParamName 'initial_camapign_id' `
        -ParamValue $InputParameters.initial_camapign_id 
    Test-CampaignID `
        -ParamName 'final_campaign_id' `
        -ParamValue $InputParameters.final_campaign_id
    Test-CampaignID `
        -ParamName 'inform_failure_campaign_id' `
        -ParamValue $InputParameters.inform_failure_campaign_id
}

#
# キャンペーン応答管理
#

function Invoke-Campaign ([string]$CampaignId) {
    Write-NxtLog -Message "Calling $($MyInvocation.MyCommand)"

    $response = Get-CampaignResponseTimeout -CampaignId $CampaignId -CampaignTimeout 60
    $status = Get-CampaignResponseStatus -Response $response
    switch ($status) {
        'fully' {
            $answer = Get-CampaignResponseAnswer -Response $response -QuestionName 'question_label'
            if ($answer -eq 'yes_label') {
                return $true
            } elseif ($answer -eq 'no_label') {
                return $false
            } else {
                throw "Unexpected answer from the user: $answer. "
            }
        }
        'timeout' { throw "Timeout on getting an answer from the user. " }
        'declined' {throw "Campaign declined by user. " }
        'postponed' {throw "Campaign postponed by user. " }
        'connectionfailed' { throw "Unable to connect to the Collector component that controls campaign notifications. " }
        'notificationfailed' { throw "Unable to notify the Collector component that controls campaign notifications. " }
            default { throw "Failed to handle campaign response: $response. " }
        }
}

#
# アプリケーション管理
#
function Invoke-RemediationAction ([hashtable]$InputParameters) {

    $process = Get-Process -Name $KANOPY_APPLICATION_PROCESS_NAME -ErrorAction SilentlyContinue
    if ($process) {
        $userResponse = Invoke-Campaign -CampaignId $InputParameters.initial_camapign_id

        if ($userResponse -eq $true) {

            try {
                Stop-Process -Name $KANOPY_APPLICATION_PROCESS_NAME -Force -ErrorAction Stop | Out-Null
                Write-StatusMessage -Message "The Kanopy process was stopped successfully."
            } catch {
                Invoke-OperationCompletedCampaign -CampaignId $InputParameters.inform_failure_campaign_id
                throw "Failed to stop the process Kanopy. Error: $_"
            }

            Start-Sleep -Seconds 2

            try {
                Start-Process -Name $KANOPY_APPLICATION_PROCESS_NAME -Force -ErrorAction Stop | Out-Null
                Write-StatusMessage -Message "The Kanopy process was started successfully."
            } catch {
                Invoke-OperationCompletedCampaign -CampaignId $InputParameters.inform_failure_campaign_id
                throw "Failed to start the process Kanopy after stopping it. Error: $_"
            }

            Invoke-OperationCompletedCampaign -CampaignId $InputParameters.final_campaign_id
        } else {
            Write-StatusMessage -Message "The user declined the remediation action."
        }
    } else {
        Write-StatusMessage -Message "The Kanopy process was not running. Application will be started."

        try {
            Start-Process -FilePath $KANOPY_APPLICATION_EXECUTABLE_PATH -ErrorAction Stop
        } catch {
            throw "Failed to start the process Kanopy. Error: $_"
        }
        Start-Sleep -Seconds 2

        $processCheck = Get-Process -Name $KANOPY_APPLICATION_PROCESS_NAME -ErrorAction SilentlyContinue
        if ($processCheck) {
            Write-StatusMessage -Message "The Kanopy process was started successfully."
        } else {
            throw "The Kanopy process was not started successfully."
        }
    }

}

#
# メインスクリプトの流れ
#
[environment]::Exit((Invoke-Main -InputParameters $MyInvocation.BoundParameters))
```

</details>

***

## スクリプトに署名する

{% hint style="info" %}
Nexthinkは、プロダクション環境でのすべてのスクリプトに署名することを推奨しています。 署名のないスクリプトは、テスト環境でのみ使用する必要があります。
{% endhint %}

### 証明書を取得する

PowerShellスクリプトに署名するには、[Set-Authenticode](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-authenticodesignature?view=powershell-7) コマンドを次のように使用します：

1. 次の方法でコード署名証明書を取得します：
   * [PowerShell証明書プロバイダー](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.security/about/about_certificate_provider?view=powershell-7)：\
     `$cert = Get-ChildItem -Path Cert:\CurrentUser\My -CodeSigningCert`
   * PFXファイル:\
     `$cert = Get-PfxCertificate -FilePath C:\Test\Mysign.pfx`
2. 証明書を使用して、リモートアクション用スクリプト、例えば`remoteaction.ps1`に署名します。 証明書の有効期限が切れた後も機能するようにタイムスタンプを追加します。 以下の例ではDigiCertのタイムスタンプサーバーを使用しています:\
   `Set-AuthenticodeSignature -FilePath .\remoteaction.ps1 -Certificate $cert -IncludeChain All -TimestampServer "http://timestamp.digicert.com"`
3. （オプション）スクリプトの署名を確認します。\
   `Get-AuthenticodeSignature .\remoteaction.ps1 -Verbose | fl`

{% hint style="danger" %}
プライベート認証機関 (CA) を使用する場合、キャッシングによるサーバーの過負荷を防ぐために、最良のOCSPプラクティスを採用してください。
{% endhint %}

### エンドポイントへの証明書の展開

デフォルトポリシー (`signed_trusted_or_nexthink`) は、追加の設定をせずにNexthinkライブラリの公式リモートアクションをデバイスで実行できるようにします。

{% hint style="info" %}
独自のリモートアクション用スクリプトを作成して署名する場合、署名証明書を**ローカル コンピュータ** **> 信頼された発行元**の証明書ストアに追加してください。
{% endhint %}

厳格な `signed_trusted` ポリシーを使用する場合、自分の証明書でライブラリおよびシステムスクリプトを再署名するか、Nexthinkコード署名証明書をMicrosoft Windowsの **Local Computer** **> Trusted Publishers** 証明書ストアに展開することができます。

{% hint style="danger" %}
コード署名証明書をWindowsの**ローカル コンピュータ** **>** **信頼された発行元**ストアに追加しない場合、システムは次のエラーを生成します： **リモートアクションを実行できませんでした：スクリプト署名が無効であるか、証明書が信頼されていません。**
{% endhint %}

プライベートCAによって生成された証明書のルート証明書がWindowsのローカル コンピュータの信頼されたルート証明機関の証明書ストアにすでに存在しない場合、追加することを確認してください。

スクリプト署名に中間証明書を使用した場合、中間証明書チェーン全体をWindowsのローカル コンピュータの中間証明機関の証明書ストアに含めます：

1. 管理者としてMicrosoft Windowsにログインします。
2. **Win+R**キーを押して実行ダイアログを開きます：
   1. **certlm.msc**と入力します。
   2. **OK**をクリックします。
3. プログラムがデバイスを変更することを許可するために、**はい**をクリックします。
4. 左側のリストで、望む証明書ストアの名前、例えば**信頼された発行元**を右クリックします。
   1. コンテキストメニューから**すべてのタスク** > **インポート...** を選択して、証明書インポートウィザードを開始します。
5. ウィザードを開始するには**次へ**をクリックします。
6. **参照**をクリックして、証明書ファイルを選択します。
7. **次へ**をクリックします。
8. **次へ**をクリックして、**以下のストアにすべての証明書を配置**ダイアログウィンドウで提案された証明書ストアを受け入れます。
9. インポートする証明書を確認し、**完了**をクリックします。

<figure><img src="https://3549141153-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeLm8O7QKZDn6z806e7Sv%2Fuploads%2Fgit-blob-45054cb238221b18beef7f77c319d9fe742ffa7d%2Fra-1699365242.png?alt=media" alt="Certificates" width="546"><figcaption></figcaption></figure>

{% hint style="info" %}
Nexthinkは、グループポリシーオブジェクト (GPO) やMicrosoft Intune Policy などの管理ツールを使用して、全デバイスに同時に証明書を展開することを推奨します。
{% endhint %}

## スクリプトの維持管理

### 比較と検証

リモートアクションスクリプトをデプロイする前に、Nexthinkによって準備された他のスクリプトと比較することが可能です。 このステップは任意ですが、初めてスクリプトを準備する場合には推奨されます。

1. Nexthinkライブラリで、**コンテンツ**を選択します。
2. **リモートアクション**でフィルターします。
3. **リモートアクション**管理ページに移動します。
4. ターゲットのオペレーティングシステムに一致するNexthinkライブラリから直接インストールされたリモートアクションスクリプトを選択します。
5. スクリプトをエクスポートし、その構文をあなたのスクリプトと比較します。

### エラーハンドリング

Nexthinkは、スクリプトを実行したPowerShellプロセスの戻り値に基づいて、リモートアクションの実行が成功したかどうかを検出します：

* 終了コードの値が0の場合、正常に実行されたことを示します。
* ゼロ以外の値はエラーを示します。

PowerShellで未処理の例外が発生すると、適切な終了コードを返さずにスクリプトが終了することがあります。 予期しないエラーを処理するために、すべてのスクリプトの冒頭に次のコードスニペットを追加することをNexthinkは推奨しています：

```powershell
 trap {
     $host.ui.WriteErrorLine($_.ToString())
     exit 1
 }
```

必須パラメータの宣言後、必要なDLL依存項目を含む部分の直下にこのデフォルトのエラーハンドラーを配置します。

### パフォーマンス測定

スクリプトのパフォーマンスとリソース消費を測定するには、[Collector 設定](https://docs.nexthink.com/platform/ja/configuring_nexthink/bringing-data-into-your-nexthink-instance/deploying-nexthink-in-non-vdi-environment/installing-collector/windows-collector-references/collector-configuration-tool-for-windows.md)ツールを使用して、Collectorのデバッグモードでログを有効にしてください。

```
nxtcfg.exe /s logmode=2
```

出力は `nxtcod.log` ファイルに保存されます。
