Initial OFAC Civil Penalties scraper

Scrapes https://ofac.treasury.gov/civil-penalties-and-enforcement-information
for all years 2003-present. Downloads PDF documents and exports metadata.json
per CGSH Publication spec (v3) to S3 experimental bucket under ofac/ prefix.

Commands: ofac-full (all years), ofac-daily (current year incremental).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Peter Foster
2026-04-09 15:29:00 +01:00
commit ad7c5d55eb
110 changed files with 5075 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
bin/\nobj/\ndata/\n*.user\n.vs/

View File

@@ -0,0 +1,93 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OFACScraper.Configuration;
namespace OFACScraper;
public class Application
{
private readonly OFACScraper _scraper;
private readonly Exporter _exporter;
private readonly CheckpointStore _checkpoint;
private readonly OFACOptions _options;
private readonly ILogger<Application> _logger;
public Application(
OFACScraper scraper,
Exporter exporter,
CheckpointStore checkpoint,
IOptions<OFACOptions> options,
ILogger<Application> logger)
{
_scraper = scraper;
_exporter = exporter;
_checkpoint = checkpoint;
_options = options.Value;
_logger = logger;
}
/// <summary>
/// Full historical scrape: all years from StartYear to current year.
/// Skips records already in checkpoint.
/// </summary>
public async Task<int> RunFullAsync(CancellationToken ct = default)
{
var currentYear = DateTime.UtcNow.Year;
_logger.LogInformation("Starting full scrape {StartYear}{EndYear}", _options.StartYear, currentYear);
var total = 0;
for (var year = _options.StartYear; year <= currentYear; year++)
{
total += await ProcessYearAsync(year, ct);
if (ct.IsCancellationRequested) break;
}
_logger.LogInformation("Full scrape complete. {Total} new records exported. DB total: {DbTotal}",
total, _checkpoint.GetTotalCount());
return 0;
}
/// <summary>
/// Daily/incremental run: scrapes current year only, exports any new records.
/// </summary>
public async Task<int> RunDailyAsync(CancellationToken ct = default)
{
var currentYear = DateTime.UtcNow.Year;
_logger.LogInformation("Starting daily scrape for {Year}", currentYear);
var newRecords = await ProcessYearAsync(currentYear, ct);
_logger.LogInformation("Daily scrape complete. {New} new records exported.", newRecords);
return 0;
}
private async Task<int> ProcessYearAsync(int year, CancellationToken ct)
{
var records = await _scraper.GetYearRecordsAsync(year, ct);
var newCount = 0;
foreach (var record in records)
{
if (ct.IsCancellationRequested) break;
if (_checkpoint.HasRecord(record.TextId))
{
_logger.LogDebug("Skipping {TextId} (already processed)", record.TextId);
continue;
}
var success = await _exporter.ExportRecordAsync(record, ct);
if (success)
{
_checkpoint.MarkProcessed(
record.TextId, record.Date, record.Name, record.PenaltyTotalUsd,
record.DocumentUrl, record.FileName, record.Year);
newCount++;
}
}
if (newCount > 0)
_logger.LogInformation("Year {Year}: exported {Count} new records", year, newCount);
return newCount;
}
}

View File

@@ -0,0 +1,79 @@
using Dapper;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OFACScraper.Configuration;
namespace OFACScraper;
/// <summary>
/// SQLite-backed store tracking which OFAC records have been processed and synced to S3.
/// </summary>
public class CheckpointStore : IDisposable
{
private readonly SqliteConnection _db;
private readonly ILogger<CheckpointStore> _logger;
public CheckpointStore(IOptions<StorageOptions> options, ILogger<CheckpointStore> logger)
{
_logger = logger;
var dbPath = options.Value.DatabasePath;
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
_db = new SqliteConnection($"Data Source={dbPath}");
_db.Open();
InitSchema();
}
public void Dispose() => _db.Dispose();
private void InitSchema()
{
_db.Execute("""
CREATE TABLE IF NOT EXISTS records (
text_id TEXT PRIMARY KEY,
date TEXT NOT NULL,
name TEXT NOT NULL,
penalty_usd REAL,
document_url TEXT NOT NULL,
file_name TEXT NOT NULL,
year INTEGER NOT NULL,
processed_at TEXT NOT NULL,
s3_synced INTEGER NOT NULL DEFAULT 0
)
""");
}
public bool HasRecord(string textId) =>
_db.ExecuteScalar<int>("SELECT COUNT(1) FROM records WHERE text_id = @textId", new { textId }) > 0;
public void MarkProcessed(string textId, DateTime date, string name, decimal? penaltyUsd,
string documentUrl, string fileName, int year)
{
_db.Execute("""
INSERT OR REPLACE INTO records (text_id, date, name, penalty_usd, document_url, file_name, year, processed_at, s3_synced)
VALUES (@textId, @date, @name, @penaltyUsd, @documentUrl, @fileName, @year, @processedAt, 0)
""",
new
{
textId,
date = date.ToString("yyyy-MM-dd"),
name,
penaltyUsd,
documentUrl,
fileName,
year,
processedAt = DateTime.UtcNow.ToString("O")
});
}
public void MarkS3Synced(string textId)
{
_db.Execute("UPDATE records SET s3_synced = 1 WHERE text_id = @textId", new { textId });
}
public int GetTotalCount() =>
_db.ExecuteScalar<int>("SELECT COUNT(1) FROM records");
public int GetYearCount(int year) =>
_db.ExecuteScalar<int>("SELECT COUNT(1) FROM records WHERE year = @year", new { year });
}

View File

@@ -0,0 +1,37 @@
namespace OFACScraper.Configuration;
public class OFACOptions
{
public const string SectionName = "OFAC";
public string BaseUrl { get; set; } = "https://ofac.treasury.gov";
public string YearUrlTemplate { get; set; } = "/civil-penalties-and-enforcement-information/{0}-enforcement-information";
public int StartYear { get; set; } = 2003;
public string UserAgent { get; set; } = "Mozilla/5.0 (compatible; OFACBot/1.0; +https://ukdataservices.co.uk)";
public int RequestDelayMs { get; set; } = 1000;
}
public class StorageOptions
{
public const string SectionName = "Storage";
public string DatabasePath { get; set; } = "data/ofac.db";
public string ExportDirectory { get; set; } = "data/exports";
public string DownloadDirectory { get; set; } = "data/downloads";
}
public class S3Options
{
public const string SectionName = "S3";
public string? BucketName { get; set; }
public string? AccessKeyId { get; set; }
public string? SecretAccessKey { get; set; }
public string Region { get; set; } = "eu-west-3";
public string Prefix { get; set; } = "ofac";
public bool IsConfigured =>
!string.IsNullOrWhiteSpace(BucketName) &&
!string.IsNullOrWhiteSpace(AccessKeyId) &&
!string.IsNullOrWhiteSpace(SecretAccessKey);
}

120
src/OFACScraper/Exporter.cs Normal file
View File

@@ -0,0 +1,120 @@
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OFACScraper.Configuration;
using OFACScraper.Helpers;
using OFACScraper.Models;
using OFACScraper.Services;
namespace OFACScraper;
public class Exporter
{
private readonly HttpClient _http;
private readonly S3UploadService _s3;
private readonly StorageOptions _storage;
private readonly S3Options _s3Options;
private readonly ILogger<Exporter> _logger;
public Exporter(
HttpClient http,
S3UploadService s3,
IOptions<StorageOptions> storage,
IOptions<S3Options> s3Options,
ILogger<Exporter> logger)
{
_http = http;
_s3 = s3;
_storage = storage.Value;
_s3Options = s3Options.Value;
_logger = logger;
}
/// <summary>
/// Exports a single OFAC record: downloads PDF, writes metadata.json, syncs to S3.
/// Returns true if the record was exported successfully.
/// </summary>
public async Task<bool> ExportRecordAsync(OFACRecord record, CancellationToken ct = default)
{
var pubId = DeterministicGuid.FromUrl(record.YearPageUrl + "#" + record.TextId);
var docId = DeterministicGuid.FromUrl(record.DocumentUrl);
var exportDir = Path.Combine(_storage.ExportDirectory, pubId.ToString());
Directory.CreateDirectory(exportDir);
// Download PDF
var localPdfPath = Path.Combine(exportDir, record.FileName);
if (!File.Exists(localPdfPath))
{
if (!await DownloadFileAsync(record.DocumentUrl, localPdfPath, ct))
{
_logger.LogError("Failed to download PDF for {TextId}", record.TextId);
return false;
}
}
// Build publication metadata
var pub = new Publication
{
Id = pubId,
TextId = record.TextId,
Source = PublicationSources.OfacCivilPenalty,
WebsiteLink = record.YearPageUrl,
Title = $"{record.Name} - OFAC Civil Penalty {record.Date:yyyy-MM-dd}",
DatePublished = new DateTimeOffset(record.Date, TimeSpan.Zero),
DateAccessed = DateTimeOffset.UtcNow,
ScraperVersion = ScraperConstants.ScraperVersion,
Documents =
[
new PublicationDocument
{
Id = docId,
Name = record.FileName,
Url = record.DocumentUrl
}
],
Tags =
[
new PublicationTag { Slug = "penalty-date", Date = new DateTimeOffset(record.Date, TimeSpan.Zero) },
new PublicationTag { Slug = "entity-name", Text = record.Name },
new PublicationTag { Slug = "penalty-amount-usd", Text = record.PenaltyTotalUsd?.ToString("F2") ?? "N/A" },
new PublicationTag { Slug = "year", Text = record.Year.ToString() }
]
};
// Write metadata.json (last — S3 sync uploads it after the PDF)
var metadataPath = Path.Combine(exportDir, "metadata.json");
var json = JsonSerializer.Serialize(pub, JsonConstants.Default);
await File.WriteAllTextAsync(metadataPath, json, ct);
// Sync this record's directory to S3
var s3Prefix = $"{_s3Options.Prefix}/{pubId}";
var syncResult = await _s3.SyncDirectoryAsync(exportDir, s3Prefix);
if (syncResult.Failed > 0)
{
_logger.LogError("S3 sync failed for {TextId}: {Failed} files failed", record.TextId, syncResult.Failed);
return false;
}
_logger.LogInformation("Exported {TextId} → S3 ({Uploaded} uploaded, {Skipped} unchanged)",
record.TextId, syncResult.Uploaded, syncResult.Skipped);
return true;
}
private async Task<bool> DownloadFileAsync(string url, string localPath, CancellationToken ct)
{
try
{
var bytes = await _http.GetByteArrayAsync(url, ct);
await File.WriteAllBytesAsync(localPath, bytes, ct);
_logger.LogDebug("Downloaded {Url} → {Path}", url, localPath);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error downloading {Url}", url);
return false;
}
}
}

View File

@@ -0,0 +1,44 @@
using System.Security.Cryptography;
using System.Text;
namespace OFACScraper.Helpers;
/// <summary>
/// Deterministic UUID v5 generation per CGSH spec (Ben Lok, 9 Mar 2026).
/// Both pub_id and doc_id use namespace 6ba7b811-9dad-11d1-80b4-00c04fd430c8 with the URL as input.
/// </summary>
public static class DeterministicGuid
{
private static readonly Guid NamespaceUrl = Guid.Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8");
public static Guid FromUrl(string url) => CreateV5(NamespaceUrl, url);
private static Guid CreateV5(Guid namespaceId, string name)
{
var namespaceBytes = namespaceId.ToByteArray();
SwapByteOrder(namespaceBytes);
var nameBytes = Encoding.UTF8.GetBytes(name);
var combined = new byte[namespaceBytes.Length + nameBytes.Length];
Buffer.BlockCopy(namespaceBytes, 0, combined, 0, namespaceBytes.Length);
Buffer.BlockCopy(nameBytes, 0, combined, namespaceBytes.Length, nameBytes.Length);
var hashBytes = SHA1.HashData(combined);
var guidBytes = new byte[16];
Array.Copy(hashBytes, guidBytes, 16);
guidBytes[6] = (byte)((guidBytes[6] & 0x0F) | 0x50);
guidBytes[8] = (byte)((guidBytes[8] & 0x3F) | 0x80);
SwapByteOrder(guidBytes);
return new Guid(guidBytes);
}
private static void SwapByteOrder(byte[] guid)
{
(guid[3], guid[0]) = (guid[0], guid[3]);
(guid[2], guid[1]) = (guid[1], guid[2]);
(guid[5], guid[4]) = (guid[4], guid[5]);
(guid[7], guid[6]) = (guid[6], guid[7]);
}
}

View File

@@ -0,0 +1,35 @@
using System.Globalization;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace OFACScraper.Helpers;
public class TagDateConverter : JsonConverter<DateTimeOffset?>
{
public override DateTimeOffset? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null) return null;
var s = reader.GetString();
if (string.IsNullOrEmpty(s)) return null;
return DateTimeOffset.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
}
public override void Write(Utf8JsonWriter writer, DateTimeOffset? value, JsonSerializerOptions options)
{
if (value is null)
writer.WriteNullValue();
else
writer.WriteStringValue(value.Value.ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture));
}
}
public static class JsonConstants
{
public static readonly JsonSerializerOptions Default = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
}

View File

@@ -0,0 +1,18 @@
namespace OFACScraper.Models;
public record OFACRecord
{
public required DateTime Date { get; init; }
public required string Name { get; init; }
public required decimal? PenaltyTotalUsd { get; init; }
public required string DocumentUrl { get; init; }
public required string FileName { get; init; }
public required int Year { get; init; }
public required string YearPageUrl { get; init; }
/// <summary>
/// Stable text identifier derived from PDF filename (without extension).
/// E.g. "20260317_tradestation" from "20260317_tradestation.pdf"
/// </summary>
public string TextId => Path.GetFileNameWithoutExtension(FileName);
}

View File

@@ -0,0 +1,81 @@
using System.Text.Json.Serialization;
using OFACScraper.Helpers;
namespace OFACScraper.Models;
public class Publication
{
[JsonPropertyName("id")]
public required Guid Id { get; init; }
[JsonPropertyName("text_id")]
public required string TextId { get; init; }
[JsonPropertyName("source")]
public required string Source { get; init; }
[JsonPropertyName("website_link")]
public required string WebsiteLink { get; init; }
[JsonPropertyName("title")]
public required string Title { get; init; }
[JsonPropertyName("date_published")]
public required DateTimeOffset DatePublished { get; init; }
[JsonPropertyName("date_accessed")]
public required DateTimeOffset DateAccessed { get; set; }
[JsonPropertyName("scraper_version")]
public required int ScraperVersion { get; init; }
[JsonPropertyName("deleted")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? Deleted { get; set; }
[JsonPropertyName("documents")]
public List<PublicationDocument> Documents { get; init; } = [];
[JsonPropertyName("tags")]
public List<PublicationTag> Tags { get; init; } = [];
}
public record PublicationDocument
{
[JsonPropertyName("id")]
public required Guid Id { get; init; }
[JsonPropertyName("name")]
public required string Name { get; init; }
[JsonPropertyName("is_attachment")]
public bool IsAttachment { get; init; } = false;
[JsonPropertyName("url")]
public required string Url { get; init; }
}
public record PublicationTag
{
[JsonPropertyName("slug")]
public required string Slug { get; init; }
[JsonPropertyName("text")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Text { get; init; }
[JsonPropertyName("date")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(TagDateConverter))]
public DateTimeOffset? Date { get; init; }
}
public static class PublicationSources
{
public const string OfacCivilPenalty = "OFAC_CIVIL_PENALTY";
}
public static class ScraperConstants
{
public const int ScraperVersion = 3;
}

View File

@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>OFACScraper</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="System.Text.Json" Version="9.0.0" />
<PackageReference Include="AWSSDK.S3" Version="3.7.400.2" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,67 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OFACScraper;
using OFACScraper.Configuration;
using OFACScraper.Services;
using Serilog;
var command = args.FirstOrDefault() ?? "ofac-daily";
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.WriteTo.File("logs/ofac-.log", rollingInterval: RollingInterval.Day,
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
try
{
Log.Information("OFAC Scraper starting. Command: {Command}", command);
var host = Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureServices((ctx, services) =>
{
services.Configure<OFACOptions>(ctx.Configuration.GetSection(OFACOptions.SectionName));
services.Configure<StorageOptions>(ctx.Configuration.GetSection(StorageOptions.SectionName));
services.Configure<S3Options>(ctx.Configuration.GetSection(S3Options.SectionName));
const string userAgent = "Mozilla/5.0 (compatible; OFACBot/1.0; +https://ukdataservices.co.uk)";
services.AddHttpClient<OFACScraper.OFACScraper>(client =>
{
client.DefaultRequestHeaders.Add("User-Agent", userAgent);
client.Timeout = TimeSpan.FromSeconds(60);
});
services.AddHttpClient<Exporter>(client =>
{
client.DefaultRequestHeaders.Add("User-Agent", userAgent);
client.Timeout = TimeSpan.FromSeconds(120);
});
services.AddSingleton<S3UploadService>();
services.AddSingleton<CheckpointStore>();
services.AddTransient<Application>();
})
.Build();
var app = host.Services.GetRequiredService<Application>();
var exitCode = command switch
{
"ofac-full" => await app.RunFullAsync(),
"ofac-daily" => await app.RunDailyAsync(),
_ => throw new ArgumentException($"Unknown command: {command}. Use ofac-full or ofac-daily.")
};
return exitCode;
}
catch (Exception ex)
{
Log.Fatal(ex, "Unhandled exception");
return 1;
}
finally
{
Log.CloseAndFlush();
}

131
src/OFACScraper/Scraper.cs Normal file
View File

@@ -0,0 +1,131 @@
using System.Globalization;
using HtmlAgilityPack;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OFACScraper.Configuration;
using OFACScraper.Models;
namespace OFACScraper;
public class OFACScraper
{
private readonly HttpClient _http;
private readonly OFACOptions _options;
private readonly ILogger<OFACScraper> _logger;
public OFACScraper(HttpClient http, IOptions<OFACOptions> options, ILogger<OFACScraper> logger)
{
_http = http;
_options = options.Value;
_logger = logger;
}
public string GetYearUrl(int year)
{
// The current year's data is on the main page, not a year subpath.
// Past years use /{year}-enforcement-information.
var currentYear = DateTime.UtcNow.Year;
if (year == currentYear)
return _options.BaseUrl + "/civil-penalties-and-enforcement-information";
return _options.BaseUrl + string.Format(_options.YearUrlTemplate, year);
}
/// <summary>
/// Fetches and parses the OFAC civil penalties table for a given year.
/// Returns all penalty records found on the page.
/// </summary>
public async Task<List<OFACRecord>> GetYearRecordsAsync(int year, CancellationToken ct = default)
{
var url = GetYearUrl(year);
_logger.LogInformation("Scraping year {Year}: {Url}", year, url);
string html;
try
{
html = await _http.GetStringAsync(url, ct);
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
_logger.LogWarning("No page found for year {Year} (404)", year);
return [];
}
var doc = new HtmlDocument();
doc.LoadHtml(html);
var table = doc.DocumentNode.SelectSingleNode("//table[contains(@class,'usa-table')]");
if (table == null)
{
_logger.LogWarning("No usa-table found for year {Year}", year);
return [];
}
var rows = table.SelectNodes(".//tbody/tr");
if (rows == null) return [];
var records = new List<OFACRecord>();
foreach (var row in rows)
{
var cells = row.SelectNodes(".//td");
if (cells == null || cells.Count < 4) continue;
// Date cell: <a href="/media/.../download?inline" title="filename.pdf">MM/DD/YYYY</a>
var dateLink = cells[0].SelectSingleNode(".//a");
if (dateLink == null) continue; // "Year to date totals" summary row
var dateText = HtmlEntity.DeEntitize(dateLink.InnerText).Trim();
if (!DateTime.TryParseExact(dateText, "MM/dd/yyyy", CultureInfo.InvariantCulture,
DateTimeStyles.None, out var date))
{
_logger.LogWarning("Could not parse date '{Date}' in year {Year}", dateText, year);
continue;
}
var docHref = dateLink.GetAttributeValue("href", "").Trim();
if (string.IsNullOrEmpty(docHref)) continue;
var docUrl = docHref.StartsWith("http") ? docHref : _options.BaseUrl + docHref;
var fileName = dateLink.GetAttributeValue("title", "").Trim();
if (string.IsNullOrEmpty(fileName))
fileName = $"{date:yyyyMMdd}_{Slugify(HtmlEntity.DeEntitize(cells[1].InnerText).Trim())}.pdf";
var name = HtmlEntity.DeEntitize(cells[1].InnerText).Trim();
var penaltyText = HtmlEntity.DeEntitize(cells[3].InnerText).Trim();
var penalty = ParsePenalty(penaltyText);
records.Add(new OFACRecord
{
Date = date,
Name = name,
PenaltyTotalUsd = penalty,
DocumentUrl = docUrl,
FileName = fileName,
Year = year,
YearPageUrl = url
});
}
_logger.LogInformation("Year {Year}: found {Count} records", year, records.Count);
if (_options.RequestDelayMs > 0)
await Task.Delay(_options.RequestDelayMs, ct);
return records;
}
private static decimal? ParsePenalty(string text)
{
if (string.IsNullOrWhiteSpace(text)) return null;
var cleaned = text.Replace("$", "").Replace(",", "").Trim();
return decimal.TryParse(cleaned, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)
? value
: null;
}
private static string Slugify(string text) =>
new string(text.ToLowerInvariant()
.Select(c => char.IsLetterOrDigit(c) ? c : '_')
.ToArray())
.Trim('_');
}

View File

@@ -0,0 +1,132 @@
using Amazon;
using Amazon.S3;
using Amazon.S3.Model;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OFACScraper.Configuration;
namespace OFACScraper.Services;
public class S3UploadService : IDisposable
{
private readonly S3Options _options;
private readonly ILogger<S3UploadService> _logger;
private readonly IAmazonS3? _s3Client;
public S3UploadService(IOptions<S3Options> options, ILogger<S3UploadService> logger)
{
_options = options.Value;
_logger = logger;
if (_options.IsConfigured)
{
var config = new AmazonS3Config
{
RegionEndpoint = RegionEndpoint.GetBySystemName(_options.Region)
};
_s3Client = new AmazonS3Client(_options.AccessKeyId, _options.SecretAccessKey, config);
_logger.LogInformation("S3 configured: bucket={Bucket} region={Region} prefix={Prefix}",
_options.BucketName, _options.Region, _options.Prefix);
}
else
{
_logger.LogWarning("S3 not configured — uploads will be skipped.");
}
}
public void Dispose() => (_s3Client as IDisposable)?.Dispose();
public async Task<bool> UploadFileAsync(string localPath, string s3Key)
{
if (_s3Client == null) return false;
try
{
await _s3Client.PutObjectAsync(new PutObjectRequest
{
BucketName = _options.BucketName,
Key = s3Key,
FilePath = localPath
});
_logger.LogDebug("Uploaded {Path} → s3://{Bucket}/{Key}", localPath, _options.BucketName, s3Key);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to upload {Path}", localPath);
return false;
}
}
/// <summary>
/// Syncs localDirectory to S3 under s3Prefix, skipping files whose MD5 matches existing S3 ETag.
/// Uploads metadata.json last so CGSH processing triggers only after all documents are present.
/// </summary>
public async Task<S3SyncResult> SyncDirectoryAsync(string localDirectory, string s3Prefix)
{
var result = new S3SyncResult();
if (_s3Client == null)
{
_logger.LogWarning("S3 not configured, skipping sync of {Path}", localDirectory);
result.NotConfigured = true;
return result;
}
if (!Directory.Exists(localDirectory))
{
result.Error = $"Directory not found: {localDirectory}";
return result;
}
// List existing objects to skip unchanged files
var existing = new Dictionary<string, string>();
var listRequest = new ListObjectsV2Request { BucketName = _options.BucketName, Prefix = s3Prefix + "/" };
ListObjectsV2Response listResponse;
do
{
listResponse = await _s3Client.ListObjectsV2Async(listRequest);
foreach (var obj in listResponse.S3Objects ?? [])
existing[obj.Key] = obj.ETag?.Trim('"') ?? "";
listRequest.ContinuationToken = listResponse.NextContinuationToken;
} while (listResponse.IsTruncated == true);
// metadata.json last — CGSH triggers on its arrival
var files = Directory.GetFiles(localDirectory, "*", SearchOption.AllDirectories)
.OrderBy(f => Path.GetFileName(f) == "metadata.json" ? 1 : 0)
.ThenBy(f => f)
.ToArray();
foreach (var file in files)
{
var relativePath = Path.GetRelativePath(localDirectory, file).Replace('\\', '/');
var s3Key = $"{s3Prefix}/{relativePath}";
if (existing.TryGetValue(s3Key, out var etag) && !string.IsNullOrEmpty(etag))
{
var localMd5 = Convert.ToHexString(
System.Security.Cryptography.MD5.HashData(File.ReadAllBytes(file))
).ToLowerInvariant();
if (localMd5 == etag) { result.Skipped++; continue; }
}
if (await UploadFileAsync(file, s3Key))
result.Uploaded++;
else
result.Failed++;
}
_logger.LogInformation("S3 sync: {Uploaded} uploaded, {Skipped} unchanged, {Failed} failed",
result.Uploaded, result.Skipped, result.Failed);
return result;
}
}
public class S3SyncResult
{
public int Uploaded { get; set; }
public int Skipped { get; set; }
public int Failed { get; set; }
public bool NotConfigured { get; set; }
public string? Error { get; set; }
}

View File

@@ -0,0 +1,27 @@
{
"OFAC": {
"BaseUrl": "https://ofac.treasury.gov",
"YearUrlTemplate": "/civil-penalties-and-enforcement-information/{0}-enforcement-information",
"StartYear": 2003,
"UserAgent": "Mozilla/5.0 (compatible; OFACBot/1.0; +https://ukdataservices.co.uk)",
"RequestDelayMs": 1000
},
"Storage": {
"DatabasePath": "/git/cgsh-ofac/data/ofac.db",
"ExportDirectory": "/git/cgsh-ofac/data/exports",
"DownloadDirectory": "/git/cgsh-ofac/data/downloads"
},
"S3": {
"BucketName": "uk-data-services-experimental-927681712454-eu-west-3-an",
"AccessKeyId": "AKIA5P7RDSFDK5MSRN6P",
"SecretAccessKey": "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",
"Region": "eu-west-3",
"Prefix": "ofac"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Extensions.Http": "Warning"
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,12 @@
{
"runtimeOptions": {
"tfm": "net8.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "8.0.0"
},
"configProperties": {
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,27 @@
{
"OFAC": {
"BaseUrl": "https://ofac.treasury.gov",
"YearUrlTemplate": "/civil-penalties-and-enforcement-information/{0}-enforcement-information",
"StartYear": 2003,
"UserAgent": "Mozilla/5.0 (compatible; OFACBot/1.0; +https://ukdataservices.co.uk)",
"RequestDelayMs": 1000
},
"Storage": {
"DatabasePath": "/git/cgsh-ofac/data/ofac.db",
"ExportDirectory": "/git/cgsh-ofac/data/exports",
"DownloadDirectory": "/git/cgsh-ofac/data/downloads"
},
"S3": {
"BucketName": "uk-data-services-experimental-927681712454-eu-west-3-an",
"AccessKeyId": "AKIA5P7RDSFDK5MSRN6P",
"SecretAccessKey": "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",
"Region": "eu-west-3",
"Prefix": "ofac"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Extensions.Http": "Warning"
}
}
}

View File

@@ -0,0 +1,39 @@
[2026-04-09 15:27:43 INF] OFAC Scraper starting. Command: ofac-daily
[2026-04-09 15:27:43 INF] S3 configured: bucket=uk-data-services-experimental-927681712454-eu-west-3-an region=eu-west-3 prefix=ofac
[2026-04-09 15:27:43 INF] Starting daily scrape for 2026
[2026-04-09 15:27:43 INF] Scraping year 2026: https://ofac.treasury.gov/civil-penalties-and-enforcement-information/2026-enforcement-information
[2026-04-09 15:27:43 INF] Start processing HTTP request GET https://ofac.treasury.gov/civil-penalties-and-enforcement-information/2026-enforcement-information
[2026-04-09 15:27:43 INF] Sending HTTP request GET https://ofac.treasury.gov/civil-penalties-and-enforcement-information/2026-enforcement-information
[2026-04-09 15:27:54 INF] Received HTTP response headers after 10959.9307ms - 404
[2026-04-09 15:27:54 INF] End processing HTTP request after 10981.345ms - 404
[2026-04-09 15:27:54 WRN] No page found for year 2026 (404)
[2026-04-09 15:27:54 INF] Daily scrape complete. 0 new records exported.
[2026-04-09 15:28:07 INF] OFAC Scraper starting. Command: ofac-daily
[2026-04-09 15:28:08 INF] S3 configured: bucket=uk-data-services-experimental-927681712454-eu-west-3-an region=eu-west-3 prefix=ofac
[2026-04-09 15:28:08 INF] Starting daily scrape for 2026
[2026-04-09 15:28:08 INF] Scraping year 2026: https://ofac.treasury.gov/civil-penalties-and-enforcement-information
[2026-04-09 15:28:08 INF] Start processing HTTP request GET https://ofac.treasury.gov/civil-penalties-and-enforcement-information
[2026-04-09 15:28:08 INF] Sending HTTP request GET https://ofac.treasury.gov/civil-penalties-and-enforcement-information
[2026-04-09 15:28:17 INF] Received HTTP response headers after 9491.3954ms - 200
[2026-04-09 15:28:17 INF] End processing HTTP request after 9512.7974ms - 200
[2026-04-09 15:28:17 INF] Year 2026: found 3 records
[2026-04-09 15:28:18 INF] Start processing HTTP request GET https://ofac.treasury.gov/media/935351/download?inline
[2026-04-09 15:28:18 INF] Sending HTTP request GET https://ofac.treasury.gov/media/935351/download?inline
[2026-04-09 15:28:27 INF] Received HTTP response headers after 8354.172ms - 200
[2026-04-09 15:28:27 INF] End processing HTTP request after 8354.6368ms - 200
[2026-04-09 15:28:28 INF] S3 sync: 2 uploaded, 0 unchanged, 0 failed
[2026-04-09 15:28:28 INF] Exported 20260317_tradestation → S3 (2 uploaded, 0 unchanged)
[2026-04-09 15:28:28 INF] Start processing HTTP request GET https://ofac.treasury.gov/media/935041/download?inline
[2026-04-09 15:28:28 INF] Sending HTTP request GET https://ofac.treasury.gov/media/935041/download?inline
[2026-04-09 15:28:36 INF] Received HTTP response headers after 8463.9922ms - 200
[2026-04-09 15:28:36 INF] End processing HTTP request after 8465.4544ms - 200
[2026-04-09 15:28:37 INF] S3 sync: 2 uploaded, 0 unchanged, 0 failed
[2026-04-09 15:28:37 INF] Exported 20260225_individual → S3 (2 uploaded, 0 unchanged)
[2026-04-09 15:28:37 INF] Start processing HTTP request GET https://ofac.treasury.gov/media/935006/download?inline
[2026-04-09 15:28:37 INF] Sending HTTP request GET https://ofac.treasury.gov/media/935006/download?inline
[2026-04-09 15:28:47 INF] Received HTTP response headers after 10407.4459ms - 200
[2026-04-09 15:28:47 INF] End processing HTTP request after 10408.1018ms - 200
[2026-04-09 15:28:48 INF] S3 sync: 2 uploaded, 0 unchanged, 0 failed
[2026-04-09 15:28:48 INF] Exported 20260212_img_academy → S3 (2 uploaded, 0 unchanged)
[2026-04-09 15:28:48 INF] Year 2026: exported 3 new records
[2026-04-09 15:28:48 INF] Daily scrape complete. 3 new records exported.

View File

@@ -0,0 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")]

View File

@@ -0,0 +1,22 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("OFACScraper")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
[assembly: System.Reflection.AssemblyProductAttribute("OFACScraper")]
[assembly: System.Reflection.AssemblyTitleAttribute("OFACScraper")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
// Generated by the MSBuild WriteCodeFragment class.

View File

@@ -0,0 +1 @@
c881e2e82ffe2e57dac7d53046c22b3e0303d8180aa5c669e5d28fdd74a6b73f

View File

@@ -0,0 +1,13 @@
is_global = true
build_property.TargetFramework = net8.0
build_property.TargetPlatformMinVersion =
build_property.UsingMicrosoftNETSdkWeb =
build_property.ProjectTypeGuids =
build_property.InvariantGlobalization =
build_property.PlatformNeutralAssembly =
build_property.EnforceExtendedAnalyzerRules =
build_property._SupportedPlatformList = Linux,macOS,Windows
build_property.RootNamespace = OFACScraper
build_property.ProjectDir = /git/cgsh-ofac/src/OFACScraper/
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =

View File

@@ -0,0 +1,8 @@
// <auto-generated/>
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Threading;
global using global::System.Threading.Tasks;

View File

@@ -0,0 +1 @@
644869aaa0ef17c645a59f18b03edb4fc63846d651862b56b6b165ca9d6a8716

View File

@@ -0,0 +1,85 @@
/git/cgsh-ofac/src/OFACScraper/obj/Debug/net8.0/OFACScraper.csproj.AssemblyReference.cache
/git/cgsh-ofac/src/OFACScraper/obj/Debug/net8.0/OFACScraper.GeneratedMSBuildEditorConfig.editorconfig
/git/cgsh-ofac/src/OFACScraper/obj/Debug/net8.0/OFACScraper.AssemblyInfoInputs.cache
/git/cgsh-ofac/src/OFACScraper/obj/Debug/net8.0/OFACScraper.AssemblyInfo.cs
/git/cgsh-ofac/src/OFACScraper/obj/Debug/net8.0/OFACScraper.csproj.CoreCompileInputs.cache
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/appsettings.json
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/OFACScraper
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/OFACScraper.deps.json
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/OFACScraper.runtimeconfig.json
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/OFACScraper.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/OFACScraper.pdb
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/AWSSDK.Core.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/AWSSDK.S3.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Dapper.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/HtmlAgilityPack.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Data.Sqlite.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Configuration.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Configuration.Abstractions.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Configuration.Binder.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Configuration.CommandLine.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Configuration.EnvironmentVariables.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Configuration.FileExtensions.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Configuration.Json.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Configuration.UserSecrets.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.DependencyInjection.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Diagnostics.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Diagnostics.Abstractions.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.FileProviders.Abstractions.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.FileProviders.Physical.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.FileSystemGlobbing.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Hosting.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Hosting.Abstractions.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Http.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.Abstractions.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.Configuration.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.Console.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.Debug.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.EventLog.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.EventSource.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Options.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Options.ConfigurationExtensions.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Primitives.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Serilog.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Serilog.Extensions.Hosting.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Serilog.Extensions.Logging.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Serilog.Sinks.Console.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/Serilog.Sinks.File.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/SQLitePCLRaw.batteries_v2.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/SQLitePCLRaw.core.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/SQLitePCLRaw.provider.e_sqlite3.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/System.Diagnostics.EventLog.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/System.IO.Pipelines.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/System.Text.Encodings.Web.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/System.Text.Json.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/browser-wasm/nativeassets/net8.0/e_sqlite3.a
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/linux-arm/native/libe_sqlite3.so
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/linux-arm64/native/libe_sqlite3.so
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/linux-armel/native/libe_sqlite3.so
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/linux-mips64/native/libe_sqlite3.so
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/linux-musl-arm/native/libe_sqlite3.so
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/linux-musl-arm64/native/libe_sqlite3.so
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/linux-musl-x64/native/libe_sqlite3.so
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/linux-ppc64le/native/libe_sqlite3.so
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/linux-s390x/native/libe_sqlite3.so
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/linux-x64/native/libe_sqlite3.so
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/linux-x86/native/libe_sqlite3.so
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/maccatalyst-arm64/native/libe_sqlite3.dylib
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/maccatalyst-x64/native/libe_sqlite3.dylib
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/osx-arm64/native/libe_sqlite3.dylib
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/osx-x64/native/libe_sqlite3.dylib
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/win-arm/native/e_sqlite3.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/win-arm64/native/e_sqlite3.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/win-x64/native/e_sqlite3.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/win-x86/native/e_sqlite3.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/win/lib/net8.0/System.Diagnostics.EventLog.Messages.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/win/lib/net8.0/System.Diagnostics.EventLog.dll
/git/cgsh-ofac/src/OFACScraper/bin/Debug/net8.0/runtimes/browser/lib/net8.0/System.Text.Encodings.Web.dll
/git/cgsh-ofac/src/OFACScraper/obj/Debug/net8.0/OFACScra.02733415.Up2Date
/git/cgsh-ofac/src/OFACScraper/obj/Debug/net8.0/OFACScraper.dll
/git/cgsh-ofac/src/OFACScraper/obj/Debug/net8.0/refint/OFACScraper.dll
/git/cgsh-ofac/src/OFACScraper/obj/Debug/net8.0/OFACScraper.pdb
/git/cgsh-ofac/src/OFACScraper/obj/Debug/net8.0/OFACScraper.genruntimeconfig.cache
/git/cgsh-ofac/src/OFACScraper/obj/Debug/net8.0/ref/OFACScraper.dll

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More