Scan for Malware with C# / .NET

Use AttachmentScanner to scan files and URLs for viruses and malware using .NET's built-in HttpClient.

Scan a URL

The simplest way to scan — pass a URL and get back a result:

using System.Text.Json;

var scannerUrl = Environment.GetEnvironmentVariable("ATTACHMENT_SCANNER_URL")!;
var apiToken = Environment.GetEnvironmentVariable("ATTACHMENT_SCANNER_API_TOKEN")!;

var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiToken}");

var content = new StringContent(
    JsonSerializer.Serialize(new { url = "https://www.attachmentscanner.com/eicar.com" }),
    System.Text.Encoding.UTF8,
    "application/json"
);

var response = await httpClient.PostAsync($"{scannerUrl}/v1.0/scans", content);
var json = await response.Content.ReadAsStringAsync();

Console.WriteLine(json);
// {"status":"found","matches":["Eicar-Test-File-Signature"],...}

Scan a File

Upload a file directly from disk:

var multipart = new MultipartFormDataContent();
var fileStream = File.OpenRead("/path/to/file.pdf");
multipart.Add(new StreamContent(fileStream), "file", "file.pdf");

var response = await httpClient.PostAsync($"{scannerUrl}/v1.0/scans", multipart);
var json = await response.Content.ReadAsStringAsync();

Console.WriteLine(json);
// {"status":"ok",...} if clean, {"status":"found",...} if malware detected

Async Scan with Callback

For large files or non-blocking workflows, use async: true with a callback URL. The API returns immediately with a pending status and POSTs the result to your callback when the scan completes:

// 1. Submit the scan
var content = new StringContent(
    JsonSerializer.Serialize(new {
        url = "https://example.com/large-file.zip",
        @async = true,
        callback = "https://your-app.com/webhooks/scan-complete"
    }),
    System.Text.Encoding.UTF8,
    "application/json"
);

var response = await httpClient.PostAsync($"{scannerUrl}/v1.0/scans", content);
var scan = JsonSerializer.Deserialize<JsonElement>(
    await response.Content.ReadAsStringAsync()
);

Console.WriteLine(scan.GetProperty("id"));     // "a1b2c3d4-..."
Console.WriteLine(scan.GetProperty("status")); // "pending"
// 2. Handle the callback (Minimal API example)
app.MapPost("/webhooks/scan-complete", async (HttpRequest req) =>
{
    var result = await req.ReadFromJsonAsync<JsonElement>();
    var status = result.GetProperty("status").GetString();

    if (status == "found")
    {
        // quarantine or delete the file
    }

    return Results.Json(new { received = true });
});

Or poll for results instead:

// Poll for results (alternative to callback)
var result = await httpClient.GetStringAsync($"{scannerUrl}/v1.0/scans/{scanId}");
Console.WriteLine(result); // includes "status": "ok", "found", "pending", etc.

Typed

Type-safe wrapper with full response types:

using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;

record ScanResult(
    string Id,
    string Status,
    string? Callback,
    string? Url,
    string? Filename,
    [property: JsonPropertyName("content_length")] long? ContentLength,
    string? Md5,
    string? Sha256,
    string[] Matches,
    [property: JsonPropertyName("created_at")] string CreatedAt,
    [property: JsonPropertyName("updated_at")] string UpdatedAt
);

class AttachmentScanner(string baseUrl, string apiToken)
{
    private readonly HttpClient _http = new()
    {
        DefaultRequestHeaders = { { "Authorization", $"Bearer {apiToken}" } }
    };

    public async Task<ScanResult> ScanUrlAsync(string url)
    {
        var response = await _http.PostAsJsonAsync(
            $"{baseUrl}/v1.0/scans", new { url }
        );
        return (await response.Content.ReadFromJsonAsync<ScanResult>())!;
    }

    public async Task<ScanResult> ScanFileAsync(string filePath)
    {
        var multipart = new MultipartFormDataContent();
        multipart.Add(new StreamContent(File.OpenRead(filePath)), "file", Path.GetFileName(filePath));

        var response = await _http.PostAsync($"{baseUrl}/v1.0/scans", multipart);
        return (await response.Content.ReadFromJsonAsync<ScanResult>())!;
    }

    public async Task<ScanResult> ScanUrlAsync(string url, string callback)
    {
        var response = await _http.PostAsJsonAsync(
            $"{baseUrl}/v1.0/scans",
            new { url, @async = true, callback }
        );
        return (await response.Content.ReadFromJsonAsync<ScanResult>())!;
    }

    public async Task<ScanResult> GetScanAsync(string id)
    {
        return (await _http.GetFromJsonAsync<ScanResult>($"{baseUrl}/v1.0/scans/{id}"))!;
    }
}
// Usage
var scanner = new AttachmentScanner(
    Environment.GetEnvironmentVariable("ATTACHMENT_SCANNER_URL")!,
    Environment.GetEnvironmentVariable("ATTACHMENT_SCANNER_API_TOKEN")!
);

var result = await scanner.ScanUrlAsync("https://www.attachmentscanner.com/eicar.com");

if (result.Status == "found")
{
    Console.WriteLine($"Malware detected: {string.Join(", ", result.Matches)}");
}

Minimal API Integration

A full .NET minimal API that accepts file uploads and scans them:

using System.Text.Json;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var scannerUrl = Environment.GetEnvironmentVariable("ATTACHMENT_SCANNER_URL")!;
var apiToken = Environment.GetEnvironmentVariable("ATTACHMENT_SCANNER_API_TOKEN")!;

var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiToken}");

// Scan by URL
app.MapPost("/scan/url", async (HttpRequest req) =>
{
    var body = await req.ReadFromJsonAsync<UrlRequest>();
    var content = new StringContent(
        JsonSerializer.Serialize(new { url = body!.Url }),
        System.Text.Encoding.UTF8,
        "application/json"
    );
    var response = await httpClient.PostAsync($"{scannerUrl}/v1.0/scans", content);
    var result = await response.Content.ReadAsStringAsync();
    return Results.Content(result, "application/json");
});

// Scan by file upload
app.MapPost("/scan/file", async (HttpRequest req) =>
{
    var form = await req.ReadFormAsync();
    var file = form.Files["file"]!;

    var multipart = new MultipartFormDataContent();
    var stream = file.OpenReadStream();
    var streamContent = new StreamContent(stream);
    multipart.Add(streamContent, "file", file.FileName);

    var response = await httpClient.PostAsync($"{scannerUrl}/v1.0/scans", multipart);
    var result = await response.Content.ReadAsStringAsync();
    return Results.Content(result, "application/json");
});

// Webhook callback handler
app.MapPost("/webhooks/scan-complete", async (HttpRequest req) =>
{
    var result = await req.ReadFromJsonAsync<JsonElement>();
    var status = result.GetProperty("status").GetString();

    if (status == "found")
    {
        // quarantine or delete the file
    }
    else if (status == "ok")
    {
        // file is clean — move to final storage
    }

    return Results.Json(new { received = true });
});

app.Run("http://0.0.0.0:3000");

record UrlRequest(string Url);

curl Equivalent

# Scan by URL
curl -X POST https://YOUR_SCANNER_URL/v1.0/scans \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://www.attachmentscanner.com/eicar.com"}'

# Scan by file upload
curl -X POST https://YOUR_SCANNER_URL/v1.0/scans \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -F "file=@/path/to/file.pdf"

# Async scan with callback
curl -X POST https://YOUR_SCANNER_URL/v1.0/scans \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/file.zip", "async": true, "callback": "https://your-app.com/webhooks/scan-complete"}'