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 には FindFirstStreamW
や FindNextStreamW
など代替データストリームを検索・列挙する関数が提供されている。また,代替データストリームの削除はふつうに DeleteFile
で可能だ。ただし,サブディレクトリ以下のファイル検索が面倒である。
一方,C# になるとファイル検索は非常に容易にできる。ただし,代替データストリームを直接取り扱うことはできないので P/Invoke 経由で Win32 API を呼び出す必要があり,とても面倒である。
PowerShell はファイル検索も簡単であり,代替データストリームを直接取り扱う機能もあることから PowerShell スクリプトで作成することにした。
4.2 代替データストリームの削除方法
代替データストリームの Zone.Identifier
のみを削除するのであれば,Unblock-File
コマンドで良い。しかし。代替データストリームとしては他に SmartScreen
というものがあることを確認しており,それらも削除するためには Remove-Item
コマンドを使う必要がある。
4.3 実装コード
実装コードを以下に示す。
#-------------------------------------------------------------------------------
# 代替データストリームユーティリティ
#
# 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. 参考文献
- Windowsでインターネットからダウンロードしたファイルのブロックについて - Qiita
- 代替データストリーム(ADS)について色々調べてみた - Qiita
- 代替ストリーム(ADS)についてのメモ - Qiita
- ADSについて調べてみた - Qiita
- 代替データストリームを利用した1日1回起動スクリプト - Qiita
- F#からNTFSの代替データストリームを利用する方法 - Qiita
- インターネットからダウンロードしたファイルはZone.Identifierでセキュリティ管理をする - ASCII
- dirやPowerShellでNTFSの代替データストリーム情報を表示する - itmedia
- streamsコマンドでNTFSの代替データストリーム情報を表示/削除する - itmedia
- 代替データストリームの取得はNtQueryInformationFile一択 - のふ処
- 名前に角括弧を含むファイルをGet-ChildItemやCopy-Itemで取り扱う方法 Windows PowerShell - note
- Streams v1.6 - microsoft