2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ダウンロードしたファイルのブロックを解除する PowerShell スクリプトをささっと作る

Last updated at Posted at 2024-09-15

0. はじめに

いつ頃からだろうか(Windows XP の頃?)インターネットからダウンロードしたファイルを実行しようとするとセキュリティ上の注意喚起が行われるようになった。まあ,インターネットからダウンロードした実行ファイルにはリスクがあるので当然だと思うが,近年,会社の共用ファイルサーバーが SharePoint に移行したため,Excel や PowerPoint などのマイクロソフトオフィスのファイルを開こうとしても注意喚起が行われるのだ。
※まあ VBA マクロとかあるから当然と言えば当然だけどね。

で,ダウンロードしたファイルを一つずつ右クリックして「許可する」にチェックを入れなくてはならない。はっきり言って面倒である。

1. 代替データストリームの確認方法

インターネットからダウンロードしたファイルって,どんな感じでマーキングされているのだろうか?長年疑問に思っていたが,最近になって Windows の NTFS というファイルシステムの代替データストリーム(Alternate Data Stream)という技術を用いて実現していることが分かった。詳しくは参考文献を参照されたい。

コマンドプロンプトからは dir /r で確認できる。

C:\Download>dir /r
 ドライブ C のボリューム ラベルは Windows です
 ボリューム シリアル番号は ****-**** です

 C:\Download のディレクトリ

2024/08/18  18:51    <DIR>          .
2024/08/18  18:51    <DIR>          ..
2024/08/18  18:50            20,073 棋士名一覧20230926.xlsx
                              1,239 棋士名一覧20230926.xlsx:Zone.Identifier:$DATA
               1 個のファイル              20,073 バイト
               2 個のディレクトリ  ***,***,***,*** バイトの空き領域

どこからダウンロードしたのかについては more コマンドで分かる。ただし,リダイレクトしないといけないのが辛いところ。

C:\Download>more < 棋士名一覧20230926.xlsx:Zone.Identifier
[ZoneTransfer]
ZoneId=3
HostUrl=https://*** ~ 中略 ~ ***

ちなみにイントラネットからダウンロードしたら ZoneId=2 になる場合もあるようだ。また HostUrl は単に about:internet とのみ記載される場合もあるようだ。

で,コマンドプロンプトからこの代替データストリームを削除する標準的な方法が用意されていないっていうのが問題だ。

2. Sysinternals 社のツールを使う

ふつうの人は Sysinternals 社の Streams というツールを使ったほうがいい。ちなみに Sysinternals 社はマイクロソフト社の完全子会社とのこと。Streams はコマンドラインから実行するコンソールアプリケーションだが,初回起動時に下記のようなライセンス条項の確認画面が表示される。この条項に異論が無ければ(および社内ルールに違反しなければ)このツールを使うのが良いだろう。

3. お品書き(仕様案)

ええと,ライセンス条項の英文を読むのが面倒だったので,自分で作ることにした。

  • コマンドプロンプトから実行するコンソールアプリケーションであること。
  • Sysinternals 社の Streams と同様に複数ファイル(ワイルドカード可)を指定でき,サブディレクトリ以下も検索できること。
  • 代替データストリームが存在するファイルを一覧できること。
  • Sysinternals 社の Streams と同様に代替データストリームを削除できること。
  • Sysinternals 社の Streams と異なり,代替データストリームの内容を表示できること。
    これは,どこからダウンロードしたのか忘れた際に役に立つ機能だ。

4. 実装について

4.1 言語選定

この種のアプリケーションを開発するのにあたり一番悩ましいのは開発言語の選定である。

C 言語というか,Win32 API には FindFirstStreamWFindNextStreamW など代替データストリームを検索・列挙する関数が提供されている。また,代替データストリームの削除はふつうに DeleteFile で可能だ。ただし,サブディレクトリ以下のファイル検索が面倒である。

一方,C# になるとファイル検索は非常に容易にできる。ただし,代替データストリームを直接取り扱うことはできないので P/Invoke 経由で Win32 API を呼び出す必要があり,とても面倒である。

PowerShell はファイル検索も簡単であり,代替データストリームを直接取り扱う機能もあることから PowerShell スクリプトで作成することにした。

4.2 代替データストリームの削除方法

代替データストリームの Zone.Identifier のみを削除するのであれば,Unblock-File コマンドで良い。しかし。代替データストリームとしては他に SmartScreen というものがあることを確認しており,それらも削除するためには Remove-Item コマンドを使う必要がある。

4.3 実装コード

実装コードを以下に示す。

AdsUtil.ps1
#-------------------------------------------------------------------------------
# 代替データストリームユーティリティ
#
# 2024-08-18 作成
# 2024-08-19 ディレクトリ名に角括弧 [...] が付く場合に対応
#            Special Thanks To: https://note.com/yasu_yukimaru/n/n638b8b4f3f68
# 2024-08-20 ファイルにアクセス権限がない場合の例外処理を追加
#-------------------------------------------------------------------------------
# グローバル変数
#-------------------------------------------------------------------------------
$Command   = 'LIST';	# コマンド:'LIST', 'VIEW', 'DELETE'
$Pattern   = @()		# ファイル検索パターン
$Recursive = $false		# 再帰検索フラグ
$Count     = 0			# 代替データストリームの数
#-------------------------------------------------------------------------------
# ヘルプメッセージ
#-------------------------------------------------------------------------------
function Show-Help() {
	Write-Host '代替データストリーム(複数可)を表示または削除します。'
	Write-Host ''
	Write-Host 'ADSUTIL(.PS1) [/S] [/V] [/D] [ドライブ:][パス][ファイル名]'
	Write-Host ''
	Write-Host '/S サブディレクトリを検索します。'
	Write-Host '/V 代替データストリームの内容を表示します。'
	Write-Host '/D 代替データストリームを削除します。'
	Exit -1
}
#-------------------------------------------------------------------------------
# コマンド実行
#-------------------------------------------------------------------------------
function Exec-Command( $filename ) {
	try {
		$items = Get-Item -LiteralPath $filename -Stream *
	} catch {
		Write-Host "ファイル $filename の代替データストリーム取得に失敗しました!!"
		Return
	}
	foreach( $item in $items ) {
		if( $item.Stream -eq ':$DATA' ) {
			continue
		}
		Write-Host ($filename + ':' + $item.Stream)
		if( $Command -eq "LIST" ) {
			# 何もしない
		} elseif( $Command -eq 'VIEW' ) {
			Get-Content -LiteralPath $filename -Stream $item.Stream
			Write-Host ''
		} elseif( $Command -eq 'DELETE' ) {
			Remove-Item -LiteralPath $filename -Stream $item.Stream
		}
		$SCRIPT:Count++
	}
}
#-------------------------------------------------------------------------------
# ファイル検索
#-------------------------------------------------------------------------------
function Search-File( $pat ) {
	$path = Split-Path -Path $pat -Parent
	$base = Split-Path -Path $pat -Leaf
	if( $path -eq '' ) {
		$path = '.'
	} elseif( !(Test-Path -LiteralPath $path) ) {
		Write-Host "ディレクトリ $path は存在しません!!"
		Exit -1
	}
	$path = Resolve-Path -LiteralPath $path
	if( $Recursive ) {
		$files = Get-ChildItem -LiteralPath $path -Filter $base -Recurse
	} else {
		$files = Get-ChildItem -LiteralPath $path -Filter $base
	}
	foreach( $file in $files ) {
		Exec-Command $file.FullName
	}
}
#-------------------------------------------------------------------------------
# オプション解析
#-------------------------------------------------------------------------------
foreach( $arg in $ARGS ) {
	if( $arg -like '[-/]S' ) {
		$Recursive = $true
	} elseif( $arg -like '[-/]V' ) {
		$Command = 'VIEW'
	} elseif( $arg -like '[-/]D' ) {
		$Command = 'DELETE'
	} elseif( $arg -like '[-/][H\?]' ) {
		Show-Help
	} elseif( $arg -like '[-/]*' ) {
		Write-Host "不正なオプション $arg を指定しました!!"
		Exit -1
	} else {
		$Pattern += $arg
	}
}
#-------------------------------------------------------------------------------
# ファイルの指定が無い場合,カレントディレクトリの全てのファイルを対象とする
#-------------------------------------------------------------------------------
if( $Pattern.length -eq 0 ) {
	$Pattern += '*'
}
#-------------------------------------------------------------------------------
# ファイル検索
#-------------------------------------------------------------------------------
foreach( $pat in $Pattern ) {
	Search-File $pat
}
#-------------------------------------------------------------------------------
# 結果出力
#-------------------------------------------------------------------------------
if( $Command -eq 'DELETE' ) {
	Write-Host "$Count 個の代替データストリームを削除しました。"
} else {
	Write-Host "$Count 個の代替データストリームがあります。"
}
Exit 0

5. 実行例

オプション /? または /H を付けるとヘルプメッセージが表示される。

ヘルプメッセージ
C:\Download>adsutil /?
代替データストリーム(複数可)を表示または削除します。

ADSUTIL(.PS1) [/S] [/V] [/D] [ドライブ:][パス][ファイル名]

/S サブディレクトリを検索します。
/V 代替データストリームの内容を表示します。
/D 代替データストリームを削除します。

引数なしで実行するとカレントディレクトリの全てのファイルを対象とし,代替データストリームが存在するファイルの一覧を表示する。

代替データストリームの一覧表示
C:\Download>adsutil
C:\Download\棋士名一覧20230926.xlsx:Zone.Identifier
1 個の代替データストリームがあります。

オプション /V を付けると代替データストリームの内容を表示する。

代替データストリームの内容を表示
C:\Download>adsutil /v
C:\Download\棋士名一覧20230926.xlsx:Zone.Identifier
[ZoneTransfer]
ZoneId=3
HostUrl=https://*** ~ 中略 ~ ***

1 個の代替データストリームがあります。

オプション /D を付けると代替データストリームを削除する。

代替データストリームを削除
C:\Download>adsutil /d
C:\Download\棋士名一覧20230926.xlsx:Zone.Identifier
1 個の代替データストリームを削除しました。

C:\Download>adsutil
0 個の代替データストリームがあります。

6. 参考文献

2
3
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?