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

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

リモートアクションの主な使用事例は、デバイスからのオンデマンドデータ収集、自己修復タスク、構成設定の変更です。

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

リモートアクション用スクリプトを作成するにあたっての詳細は、以下のドキュメントを参照してください:

スクリプトの作成

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

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

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

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

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

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

出力変数の作成

スクリプトを実行すると、オンデマンドデータとして保存したい出力が生成されることがあります。 Nexthinkは.NETアセンブリ(nxtremoteactions.dll)を提供しており、これはCollectorと同時に社員のデバイスにインストールされます。 このアセンブリには、結果をデータ層に書き込むためのメソッドを提供する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]

  • 最小: 0

  • 最大: 4 294 967 295

WriteOutputFloat

[float]

  • 最小: -3.4E+38

  • 最大: 3.4E+38

WriteOutputSize

[float]

  • 最小: 0

  • 最大: 3.4E+38

WriteOutputRatio

[float]

WriteOutputBitRate

[float]

WriteOutputDateTime

[DateTime]

DD.MM.YYYY@HH:MM

WriteOutputDuration

[TimeSpan]

  • 最小: 0 ms

  • 最大: 49日

  • ミリ秒単位の精度

WriteOutputStringList

[string[]]

Stringと同様

キャンペーンの実施

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

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

  • キャンペーンはリモートアクショントリガーを持ち、公開されている必要があります。

  • リモートアクションのスクリプトは次のいずれかで実行できます:

    • アクションが特別な権限を必要としない場合、社員のコンテキストで。

    • アクションが管理特権を必要とする場合、ローカルシステムアカウントのコンテキストで。

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

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

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

キャンペーンのNQL IDまたはUIDを取得する方法の詳細は、キャンペーンをトリガーするドキュメントを参照してください。

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

キャンペーンとインタラクションするには、リモートアクションスクリプトは、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の集中保護または「取り込み禁止」ルールのため表示できません。 詳細は、キャンペーンの受信率を制限するドキュメントを参照してください。

string[] GetResponseAnswer(NxTrayResp response, string questionLabel)

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

  • 単一回答の質問の場合、返された文字列配列には一つの要素しかありません。

  • 複数回答の質問の場合、返される文字列配列には、社員が選択した回答の数だけ要素が含まれます。 任意の自由テキストは無視されます。

  • 社員がキャンペーンに完全に回答していない場合、たとえばステータスがfullyでない場合、返される文字列配列は空になります。 任意の自由テキストは無視されます。

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

スクリプトのエンコード

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

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

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

コード例

キャンペーンの呼び出し

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

$result = [Nxt.CampaignAction]::RunCampaign($CampaignUid, $maxWaitTimeinSeconds)
$status = [Nxt.CampaignAction]::GetResponseStatus($result)
if ($status -eq "fully") {
        Write-Output "Campaign succeeded"
} else {
        Write-Output "Status is $status"
}
キャンペーン応答へのアクセス

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

# Function to get campaign response
function Get-CampaignResponse ([string]$CampaignId) {
    return [nxt.campaignaction]::RunCampaign($CampaignId, $CAMPAIGN_TIMEOUT)
}
# Function to get campaign status
function Get-CampaignResponseStatus ($Response) {
    return [nxt.campaignaction]::GetResponseStatus($Response)
}

# Function to get response answers
function Get-CampaignResponseAnswer ($Response, [string]$QuestionName) {
    return [nxt.campaignaction]::GetResponseAnswer($Response, $QuestionName)[0]
}

#get campaign response
$campaignResponse = $null
$campaignResponse = Get-CampaignResponse -CampaignId $campaignId

# Get campaign status
$status = $null
$status = Get-CampaignResponseStatus -Response $campaignResponse
Write-Host "The response status is $status"

# Get response answers
$answersArray = $null
$answer = Get-CampaignResponseAnswer -Response $campaignResponse -QuestionName "Question1"
Write-Host "The answer is $answer"
タイムアウト付きキャンペーンの実行

この例では、リモートアクションは指定した時間が経過するとタイムアウトし、クローズするキャンペーンを実行するように設定されています。入力は秒単位で行われます。

# Run a campaign with timeout
# timeout is in seconds (100s or 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 "Campaign succeeded"
} else {
        Write-Output "Status is $status"
}
非ブロッキングキャンペーンの実行

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

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

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

Invoke-OperationCompletedCampaign -CampaignId $mycampaignId
特定のキャンペーン応答に応じたアプリケーションの起動

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

リモートアクションは、キャンペーンIDと入力として秒単位のタイムアウトを使用して、Nxt.CampaignAction]::RunCampaign関数を用いてキャンペーンを実行します。 その後、ユーザーの応答や応答の欠如を収集し、それに基づいてステータスを判断します。 ユーザーがyesと応答した場合、リモートアクションはプロセスを開始します。この場合は、Notepadです。

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") {
        # user has confirmed - let's do some actions:
        Start-Process notepad.exe
    }
}
指定のアプリケーションがデバイスに存在するかどうかの確認

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

#
# 入力パラメータの定義
#
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



#
# メイン呼び出し
#
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))
指定のアプリケーションがデバイスに存在するかどうかの確認: エラーハンドリング

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

#
# 入力パラメータの定義
#
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



#
# メイン呼び出し
#
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管理
#
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))
指定のアプリケーションがデバイスに存在するかどうかの確認: エラー修復

この例では、リモートアクションがキャンペーンを使用してユーザーにアプリケーションが既に実行中であれば再起動が必要であることを通知します。そうでなければ、アプリケーションを開始します。

#
# 入力パラメータの定義
#
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

#
# メイン呼び出し
#
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))

スクリプトの署名

証明書を取得する

PowerShellスクリプトに署名するには、以下のようにSet-Authenticodeコマンドを使用します。

  1. 次の方法でコード署名証明書を取得します。

    • PowerShell証明書プロバイダから取得: $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

プライベート証明書機関(CA)を使用する場合、OCSPを使用したベストプラクティスを守り、キャッシュによるサーバーの負荷を避けるようにしてください。

エンドポイントへの証明書の導入

デフォルトポリシー (signed_trusted_or_nexthink) により、Nexthink Library からの公式のリモートアクションを追加の設定なしにデバイス上で実行できます。

厳格な signed_trusted ポリシーを使用する場合は、ライブラリやシステムスクリプトを独自の証明書で再署名するか、Nexthink のコード署名証明書を Microsoft Windows の ローカルコンピュータ > 信頼できる発行元 証明書ストアに展開することを選択できます。

コード署名証明書を Microsoft Windows の ローカルコンピュータ > 信頼できる発行元 ストアに追加しない場合、システムは次のエラーを生成します: リモートアクションを実行できませんでした: スクリプト署名が無効か、証明書が信頼できません。

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

スクリプトの署名に中間証明書を使用した場合、ローカルコンピュータの中間証明機関証明書ストアに中間証明書の完全なチェーンを含めてください。

  1. 管理者として Microsoft Windows にログインします。

  2. Win+R キーを押して、[ファイル名を指定して実行]ダイアログを開きます。

    1. certlm.mscと入力します。

    2. OKをクリックします。

  3. はいをクリックして、このプログラムに対する変更をデバイスで許可します。

  4. 左側のリストで、希望する証明書ストアの名前を右クリックします(例: 信頼できる発行元)。

    1. コンテキストメニューから すべてのタスク > インポート... を選択して、証明書インポートウィザードを開始します。

  5. ウィザードを開始するには 次へをクリックします。

  6. 参照をクリックして、証明書ファイルを選択します。

  7. 次へをクリックします。

  8. 「証明書を次のストアにすべて配置する」ダイアログウィンドウで、提案されたストアを受け入れるために 次へをクリックします。

  9. 取り込む証明書を確認し、完了をクリックします。

スクリプトの保守

比較と検証

リモートアクションスクリプトを導入する前に、Nexthink によって準備された他のスクリプトと比較することができます。 この手順はオプションですが、初めてスクリプトを準備する場合は推奨されます。

  1. Nexthink Library で、コンテンツを選択します。

  2. リモートアクションでフィルターします。

  3. リモートアクション管理ページに移動します。

  4. ターゲットの operating system に一致する Nexthink Library から直接インストールされた任意のリモートアクションスクリプトを選択します。

  5. スクリプトをエクスポートして、その構文をあなたのスクリプトと比較します。

エラーハンドリング

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

  • 終了コードが0の値は、成功した実行を示します。

  • 非ゼロの値はエラーを示します。

PowerShell の未処理の例外は、適切な終了コードを返さずにスクリプトを終了させる可能性があります。 予期しないエラーを処理するために、すべてのスクリプトの本体を以下のコードスニペットで開始することを Nexthink は推奨します。

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

必要な DLL 依存関係のインクルードの下に、このデフォルトのエラーハンドラを配置してください。これにより、オプションの公式パラメータの宣言が続きます。

パフォーマンス測定

スクリプトのパフォーマンスとリソース消費を測定するには、Collector の設定 ツールを使用して、Collector のログをデバッグモードでオンにします。

nxtcfg.exe /s logmode=2

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

Last updated

Was this helpful?