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:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
bin/\nobj/\ndata/\n*.user\n.vs/
|
||||||
93
src/OFACScraper/Application.cs
Normal file
93
src/OFACScraper/Application.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/OFACScraper/CheckpointStore.cs
Normal file
79
src/OFACScraper/CheckpointStore.cs
Normal 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 });
|
||||||
|
}
|
||||||
37
src/OFACScraper/Configuration/AppOptions.cs
Normal file
37
src/OFACScraper/Configuration/AppOptions.cs
Normal 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
120
src/OFACScraper/Exporter.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/OFACScraper/Helpers/DeterministicGuid.cs
Normal file
44
src/OFACScraper/Helpers/DeterministicGuid.cs
Normal 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/OFACScraper/Helpers/JsonConstants.cs
Normal file
35
src/OFACScraper/Helpers/JsonConstants.cs
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
18
src/OFACScraper/Models/OFACRecord.cs
Normal file
18
src/OFACScraper/Models/OFACRecord.cs
Normal 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);
|
||||||
|
}
|
||||||
81
src/OFACScraper/Models/Publication.cs
Normal file
81
src/OFACScraper/Models/Publication.cs
Normal 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;
|
||||||
|
}
|
||||||
35
src/OFACScraper/OFACScraper.csproj
Normal file
35
src/OFACScraper/OFACScraper.csproj
Normal 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>
|
||||||
67
src/OFACScraper/Program.cs
Normal file
67
src/OFACScraper/Program.cs
Normal 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
131
src/OFACScraper/Scraper.cs
Normal 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('_');
|
||||||
|
}
|
||||||
132
src/OFACScraper/Services/S3UploadService.cs
Normal file
132
src/OFACScraper/Services/S3UploadService.cs
Normal 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; }
|
||||||
|
}
|
||||||
27
src/OFACScraper/appsettings.json
Normal file
27
src/OFACScraper/appsettings.json
Normal 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": "r6MjrnzRVlo8/tcUXhxT4YvOPhO1vV7wjwqr0UxH",
|
||||||
|
"Region": "eu-west-3",
|
||||||
|
"Prefix": "ofac"
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.Extensions.Http": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/OFACScraper/bin/Debug/net8.0/AWSSDK.Core.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/AWSSDK.Core.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/AWSSDK.S3.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/AWSSDK.S3.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Dapper.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Dapper.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/HtmlAgilityPack.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/HtmlAgilityPack.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Data.Sqlite.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Data.Sqlite.dll
Executable file
Binary file not shown.
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Configuration.Binder.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Configuration.Binder.dll
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Configuration.Json.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Configuration.Json.dll
Executable file
Binary file not shown.
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Configuration.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Configuration.dll
Executable file
Binary file not shown.
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.DependencyInjection.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.DependencyInjection.dll
Executable file
Binary file not shown.
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Diagnostics.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Diagnostics.dll
Executable file
Binary file not shown.
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.FileProviders.Physical.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.FileProviders.Physical.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.FileSystemGlobbing.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.FileSystemGlobbing.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Hosting.Abstractions.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Hosting.Abstractions.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Hosting.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Hosting.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Http.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Http.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.Abstractions.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.Abstractions.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.Configuration.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.Configuration.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.Console.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.Console.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.Debug.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.Debug.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.EventLog.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.EventLog.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.EventSource.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.EventSource.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Logging.dll
Executable file
Binary file not shown.
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Options.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Options.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Primitives.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Microsoft.Extensions.Primitives.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/OFACScraper
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/OFACScraper
Executable file
Binary file not shown.
1069
src/OFACScraper/bin/Debug/net8.0/OFACScraper.deps.json
Normal file
1069
src/OFACScraper/bin/Debug/net8.0/OFACScraper.deps.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
src/OFACScraper/bin/Debug/net8.0/OFACScraper.dll
Normal file
BIN
src/OFACScraper/bin/Debug/net8.0/OFACScraper.dll
Normal file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/OFACScraper.pdb
Normal file
BIN
src/OFACScraper/bin/Debug/net8.0/OFACScraper.pdb
Normal file
Binary file not shown.
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"runtimeOptions": {
|
||||||
|
"tfm": "net8.0",
|
||||||
|
"framework": {
|
||||||
|
"name": "Microsoft.NETCore.App",
|
||||||
|
"version": "8.0.0"
|
||||||
|
},
|
||||||
|
"configProperties": {
|
||||||
|
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/OFACScraper/bin/Debug/net8.0/SQLitePCLRaw.batteries_v2.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/SQLitePCLRaw.batteries_v2.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/SQLitePCLRaw.core.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/SQLitePCLRaw.core.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/SQLitePCLRaw.provider.e_sqlite3.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/SQLitePCLRaw.provider.e_sqlite3.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Serilog.Extensions.Hosting.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Serilog.Extensions.Hosting.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Serilog.Extensions.Logging.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Serilog.Extensions.Logging.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Serilog.Sinks.Console.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Serilog.Sinks.Console.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Serilog.Sinks.File.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Serilog.Sinks.File.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/Serilog.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/Serilog.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/System.Diagnostics.EventLog.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/System.Diagnostics.EventLog.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/System.IO.Pipelines.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/System.IO.Pipelines.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/System.Text.Encodings.Web.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/System.Text.Encodings.Web.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/System.Text.Json.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/System.Text.Json.dll
Executable file
Binary file not shown.
27
src/OFACScraper/bin/Debug/net8.0/appsettings.json
Normal file
27
src/OFACScraper/bin/Debug/net8.0/appsettings.json
Normal 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": "r6MjrnzRVlo8/tcUXhxT4YvOPhO1vV7wjwqr0UxH",
|
||||||
|
"Region": "eu-west-3",
|
||||||
|
"Prefix": "ofac"
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.Extensions.Http": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-arm/native/libe_sqlite3.so
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-arm/native/libe_sqlite3.so
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-arm64/native/libe_sqlite3.so
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-arm64/native/libe_sqlite3.so
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-armel/native/libe_sqlite3.so
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-armel/native/libe_sqlite3.so
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-mips64/native/libe_sqlite3.so
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-mips64/native/libe_sqlite3.so
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-musl-arm/native/libe_sqlite3.so
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-musl-arm/native/libe_sqlite3.so
Executable file
Binary file not shown.
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-musl-x64/native/libe_sqlite3.so
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-musl-x64/native/libe_sqlite3.so
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-ppc64le/native/libe_sqlite3.so
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-ppc64le/native/libe_sqlite3.so
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-s390x/native/libe_sqlite3.so
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-s390x/native/libe_sqlite3.so
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-x64/native/libe_sqlite3.so
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-x64/native/libe_sqlite3.so
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-x86/native/libe_sqlite3.so
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/linux-x86/native/libe_sqlite3.so
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/osx-arm64/native/libe_sqlite3.dylib
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/osx-arm64/native/libe_sqlite3.dylib
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/osx-x64/native/libe_sqlite3.dylib
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/osx-x64/native/libe_sqlite3.dylib
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/win-arm/native/e_sqlite3.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/win-arm/native/e_sqlite3.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/win-arm64/native/e_sqlite3.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/win-arm64/native/e_sqlite3.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/win-x64/native/e_sqlite3.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/win-x64/native/e_sqlite3.dll
Executable file
Binary file not shown.
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/win-x86/native/e_sqlite3.dll
Executable file
BIN
src/OFACScraper/bin/Debug/net8.0/runtimes/win-x86/native/e_sqlite3.dll
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
39
src/OFACScraper/logs/ofac-20260409.log
Normal file
39
src/OFACScraper/logs/ofac-20260409.log
Normal 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.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// <autogenerated />
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")]
|
||||||
22
src/OFACScraper/obj/Debug/net8.0/OFACScraper.AssemblyInfo.cs
Normal file
22
src/OFACScraper/obj/Debug/net8.0/OFACScraper.AssemblyInfo.cs
Normal 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.
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
c881e2e82ffe2e57dac7d53046c22b3e0303d8180aa5c669e5d28fdd74a6b73f
|
||||||
@@ -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 =
|
||||||
@@ -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;
|
||||||
BIN
src/OFACScraper/obj/Debug/net8.0/OFACScraper.assets.cache
Normal file
BIN
src/OFACScraper/obj/Debug/net8.0/OFACScraper.assets.cache
Normal file
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
644869aaa0ef17c645a59f18b03edb4fc63846d651862b56b6b165ca9d6a8716
|
||||||
@@ -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
|
||||||
BIN
src/OFACScraper/obj/Debug/net8.0/OFACScraper.dll
Normal file
BIN
src/OFACScraper/obj/Debug/net8.0/OFACScraper.dll
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user