# 원본 one-time importer 안전 감사

기준일은 2026-07-12이며, 설치된 패키지는
`Wickedness.MBNStockWebView_1.0.3.0_x64__qbv3jkvsn3aj0`이다. 이 감사에서는
원본 `Data`를 읽기 전용으로 사용했고 Oracle/MariaDB에는 SELECT만 실행했다.
운영 `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\OperatorData`와 Tornado2에는 쓰기나
명령을 보내지 않았다. 비교쌍에 한해서는 설치된 signed package의 명시적
버튼을 한 번 눌러 WebView2 `localStorage`에 8쌍과 one-time marker를 같은
envelope로 저장했다.

## 감사 시점 운영 profile 분류

FSell/VI 저장소에는 marker와 intent 없이 원본과 byte 단위로 같은 네 파일이
이미 있다.

| 파일 | 길이 | SHA-256 | 고정 원본과 일치 |
|---|---:|---|---|
| `개인.dat` | 56 | `F953366A5D9F940CBE54F05C256F8BFB0AF4674C8F4D46CCBF839A63113342FA` | 예 |
| `외국인.dat` | 58 | `45F62038E3216E80FCF22F4E923056858E9C3537BF0DC46BC359148E82DD7CD8` | 예 |
| `기관.dat` | 27 | `BC4D62D734E701744E10DD830A2D6B9AEBEBEB795BB4DABC77CDE6577F016EB7` | 예 |
| `VI발동.dat` | 276 | `46AFBA07D2C4AF50717450582DEB10EC10367701D09161BECF5E32342AD4AA45` | 예 |

- `.legacy-manual-operator-import.v1.json`: 없음
- `.legacy-manual-operator-import.v1.in-progress.json`: 없음
- importer staging 파일: 없음
- startup 상태: marker가 없는 기존 원본 파일을 그대로 쓰는 `ReadyLegacyData`
- 현재 버튼 실행 판정: `DestinationNotEmpty`, `OutcomeUnknown=false`

따라서 현재 profile에서 `원본 데이터 가져오기`를 누르면 네 파일을 덮어쓰지
않고 source read 전에 전체 import가 중단된다. marker를 임의로 만들어
`AlreadyImported`로 바꾸거나 파일을 지워 import를 재현하면 안 된다.

다음 명령은 운영 profile을 쓰지 않고 이 판정을 다시 출력한다.

```powershell
dotnet run --project .\tools\MBN_STOCK_WEBVIEW.DbSmoke\MBN_STOCK_WEBVIEW.DbSmoke.csproj `
  -c Release -p:Platform=x64 -- --manual-import-audit
```

현재 결과:

```text
MANUAL_IMPORT_AUDIT: PASS sourceFiles=4/4 operatingFiles=4/4 sourceByteIdentical=true marker=false intent=false staged=false runtime=ReadyLegacyData decision=DestinationNotEmpty outcomeUnknown=false writesAttempted=false
```

## 격리 FSell/VI 실제 원본 import

`--manual-import-fixture`는 사용자가 destination을 지정할 수 없게 닫혀 있다.
고정 원본을 읽고 `%TEMP%\MBN_STOCK_WEBVIEW-manual-import-fixture-<GUID>`에만
쓴다. 전체 실행은 1분 timeout으로 제한되며 timeout/cancel 뒤 자동 재시도하지
않는다. 다음 항목을 모두 확인한 뒤 자신이 만든 prefix의 non-reparse directory만
삭제한다.

1. 4파일 stage 및 no-overwrite commit
2. production parser read-back
3. marker-last commit과 intent 제거
4. 재시작 guard의 `ReadyImportedData`
5. 같은 fixture의 두 번째 실행 `AlreadyImported`, `OutcomeUnknown=false`
6. 원본 4파일과 운영 import 대상 6항목의 실행 전후 SHA-256 불변
7. 원본 aggregate SHA-256 `fd39a363…a2114`, FSell 15행과 VI 9건 고정

```powershell
dotnet run --project .\tools\MBN_STOCK_WEBVIEW.DbSmoke\MBN_STOCK_WEBVIEW.DbSmoke.csproj `
  -c Release -p:Platform=x64 -- --manual-import-fixture
```

실제 결과:

```text
MANUAL_IMPORT_FIXTURE: PASS rows=15 vi=9 sourceSha256=fd39a3639e574a19f174225e8e65234d6bdd4c58700bb5d10230a779a03a2114 state=ReadyImportedData secondAttempt=AlreadyImported outcomeUnknown=false sourceUnchanged=true operatingImportSetUnchanged=true
```

## `종목비교.dat` 분류

- 고정 원본: 358바이트, 8행
- SHA-256: `1EE76BC464FB1C44C8B6546E99CDC21C726C0E2C24AD344F5C19AE41BFC0D2F2`
- 실제 Oracle/MariaDB read-only 재검증: 8/8 identity 해소
- 실행 경로: `비교` 화면의 `원본 비교쌍 가져오기`
- 성공 저장 위치: package WebView2 `localStorage`의 단일 comparison envelope

감사 초기에 package WebView2가 닫힌 상태에서 LevelDB에는 87바이트
`VERSION=1` log 외 comparison key byte가 없었고 비교 화면은 `0 PAIRS`였다.
signed 1.0.3 패키지에서 `원본 비교쌍 가져오기`를 명시적으로 한 번 실행한
결과, native가 고정 원본의 SHA-256과 8행을 다시 읽고 현재 DB에서 모든
identity를 해소한 뒤 화면이 `8 PAIRS`와 `원본 8쌍 가져옴`으로 바뀌었다.
부분 저장이나 `복구 필요` 상태는 발생하지 않았다.

이 성공은 같은 package profile의 단일 comparison envelope에 pair list와
marker를 함께 기록한 결과다. 앱을 정상 종료한 뒤 설치된 signed 1.0.3을
다시 실행해 다음 상태를 확인했다.

- 비교 화면: `8 PAIRS`
- 저장 비교쌍: 원본 순서의 8개 항목 유지
- one-time gate: 완료형 `원본 8쌍 가져옴`, 다시 실행할 수 없는 상태
- 고정 원본 SHA-256: `1EE76BC464FB1C44C8B6546E99CDC21C726C0E2C24AD344F5C19AE41BFC0D2F2`로 불변

따라서 package UI 저장과 재시작 marker 영속성은 PASS다. 실행 중 LevelDB는
process lock 상태이므로 raw 파일 검색으로 live logical state를 단정하거나
파일을 복사·수정하지 않았다.

정규 DB smoke에는 실제 importer를 추가했다. 아래 명령은 원본을 읽고 8개
identity를 fresh SELECT로 해소하지만 package `localStorage`에는 쓰지 않는다.

```powershell
dotnet run --project .\tools\MBN_STOCK_WEBVIEW.DbSmoke\MBN_STOCK_WEBVIEW.DbSmoke.csproj `
  -c Release -p:Platform=x64 --no-build
```

확인 결과는 다음과 같다.

```text
LegacyComparisonImport: rows=8, sha256=1EE76BC464FB1C44C8B6546E99CDC21C726C0E2C24AD344F5C19AE41BFC0D2F2, identities=fresh-read-only
REAL_DB_SMOKE: PASS
```

현재 package profile에서는 실제 0→8 import와 정상 종료 후 재시작 marker
검증을 완료했으므로 이를 재현하려고 package family 데이터를 초기화하지
않는다. 빈 profile의 실패 fixture가 추가로 필요하면 별도 test package
identity를 사용한다. 운영 identity에서 `Remove-AppxPackage`, app data reset,
LevelDB 파일 복사·삭제로 빈 profile을 만들지 않는다.

## 차단 판정표

| 상태 | FSell/VI 판정 | 비교쌍 판정 | 자동 재시도 |
|---|---|---|---|
| 대상 파일 없음, source 정상 | import 가능 | marker 없고 envelope 정상일 때 가능 | 명시적 버튼만 |
| 기존 4파일, marker 없음 | `DestinationNotEmpty` | 해당 없음 | 금지 |
| 유효 marker 있음 | `AlreadyImported` | 버튼이 `원본 N쌍 가져옴`으로 비활성 | 금지 |
| intent 있음 | `InterruptedImport`, `OutcomeUnknown=true` | 해당 없음 | 금지, 수동 대조 |
| marker/4파일 불일치 | `InvalidImportedState` | 해당 없음 | 금지, 수동 대조 |
| split/손상 envelope | 해당 없음 | `복구 필요`, 전체 차단 | 금지, local state 대조 |
| DB timeout/identity 오류 | 해당 없음 | 저장 전 전체 취소 | 금지 |

FSell/VI 전용 화면은 원본 운영 항목의 개인·외국인·기관 또는 VI 수동 항목을
열어 진입하며, header의 `원본 데이터 가져오기`가 importer 버튼이다. 비교쌍은
상단 `비교` 업무 탭 안의 `원본 비교쌍 가져오기` 버튼으로 진입한다.
