Skip to content

Commit b89e21b

Browse files
authored
Merge pull request #41 from Blazor-Data-Orchestrator/security/block-dll-references-enforce-nuget-only
Security/block dll references enforce nuget only
2 parents f8abda9 + 3b5e839 commit b89e21b

15 files changed

Lines changed: 358 additions & 34 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,6 @@ Thumbs.db
7777
/.playwright-mcp
7878
.azure
7979
/src/BlazorDataOrchestrator.Core/BlazorDataOrchestrator.Core.csproj.lscache
80+
81+
# Auto-generated job template zip (built by scripts/Package-JobTemplate.ps1)
82+
/src/BlazorOrchestrator.Web/JobTemplate/BlazorDataOrchestrator.JobCreatorTemplate.zip
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
# Job Template Zip Automation Plan
2+
3+
## Problem
4+
5+
The `BlazorDataOrchestrator.JobCreatorTemplate` project is shipped to users as a zip file located at `src/BlazorOrchestrator.Web/JobTemplate/BlazorDataOrchestrator.JobCreatorTemplate.zip`. Today this zip is created **manually** with error-prone steps:
6+
7+
1. Copy the entire `src/BlazorDataOrchestrator.JobCreatorTemplate` directory
8+
2. Delete `bin/`, `obj/`, `Properties/` directories
9+
3. Delete `BlazorDataOrchestrator.JobCreatorTemplate.csproj.user`
10+
4. Include the root-level `JobTemplate.slnx` file
11+
5. Zip everything and place it in `src/BlazorOrchestrator.Web/JobTemplate/`
12+
13+
This is tedious, easy to forget, and can lead to stale zip files being deployed.
14+
15+
---
16+
17+
## Goals
18+
19+
- **Zero manual steps** — the zip is regenerated automatically on every build of `BlazorOrchestrator.Web`.
20+
- **Always in sync** — any change to the template project is immediately reflected in the zip.
21+
- **No extra tools** — use only MSBuild targets and built-in .NET SDK capabilities (or a small PowerShell/shell script called from MSBuild).
22+
- **CI-friendly** — works in both local dev (`dotnet build` / `aspire run`) and CI/CD pipelines.
23+
24+
---
25+
26+
## Proposed Solution
27+
28+
### Option A — MSBuild Target in `BlazorOrchestrator.Web.csproj` (Recommended)
29+
30+
Add a `<Target>` to `BlazorOrchestrator.Web.csproj` that runs **before** the build, invokes a PowerShell/shell script (or inline MSBuild tasks) to produce the zip, and places it in the `JobTemplate/` folder so it gets included as content.
31+
32+
#### Implementation Steps
33+
34+
##### 1. Create the packaging script
35+
36+
Create `scripts/Package-JobTemplate.ps1`:
37+
38+
```powershell
39+
<#
40+
.SYNOPSIS
41+
Packages the JobCreatorTemplate project into a zip for distribution.
42+
#>
43+
param(
44+
[Parameter(Mandatory)]
45+
[string]$SourceDir, # Path to src/BlazorDataOrchestrator.JobCreatorTemplate
46+
47+
[Parameter(Mandatory)]
48+
[string]$SlnxFile, # Path to JobTemplate.slnx
49+
50+
[Parameter(Mandatory)]
51+
[string]$OutputZip # Full path for the output .zip file
52+
)
53+
54+
$ErrorActionPreference = 'Stop'
55+
56+
# Create a temp staging directory
57+
$stagingDir = Join-Path ([System.IO.Path]::GetTempPath()) "JobTemplateStaging_$([guid]::NewGuid().ToString('N'))"
58+
$templateDir = Join-Path $stagingDir "BlazorDataOrchestrator.JobCreatorTemplate"
59+
60+
try {
61+
# Copy the template project to staging
62+
Copy-Item -Path $SourceDir -Destination $templateDir -Recurse
63+
64+
# Remove directories that should not be shipped
65+
$dirsToRemove = @('bin', 'obj', 'Properties')
66+
foreach ($dir in $dirsToRemove) {
67+
$target = Join-Path $templateDir $dir
68+
if (Test-Path $target) {
69+
Remove-Item $target -Recurse -Force
70+
}
71+
}
72+
73+
# Remove user-specific files
74+
$filesToRemove = @(
75+
'*.csproj.user',
76+
'execution_errors.log'
77+
)
78+
foreach ($pattern in $filesToRemove) {
79+
Get-ChildItem -Path $templateDir -Filter $pattern -Recurse | Remove-Item -Force
80+
}
81+
82+
# Copy the .slnx file into the staging root (sibling to the project folder)
83+
Copy-Item -Path $SlnxFile -Destination $stagingDir
84+
85+
# Ensure the output directory exists
86+
$outputDir = Split-Path $OutputZip -Parent
87+
if (-not (Test-Path $outputDir)) {
88+
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
89+
}
90+
91+
# Remove old zip if it exists
92+
if (Test-Path $OutputZip) {
93+
Remove-Item $OutputZip -Force
94+
}
95+
96+
# Create the zip
97+
Compress-Archive -Path "$stagingDir\*" -DestinationPath $OutputZip -Force
98+
99+
Write-Host "Created template zip: $OutputZip"
100+
}
101+
finally {
102+
# Clean up staging
103+
if (Test-Path $stagingDir) {
104+
Remove-Item $stagingDir -Recurse -Force
105+
}
106+
}
107+
```
108+
109+
##### 2. Add MSBuild target to `BlazorOrchestrator.Web.csproj`
110+
111+
```xml
112+
<!-- Auto-package the JobCreatorTemplate zip before build -->
113+
<Target Name="PackageJobTemplate" BeforeTargets="BeforeBuild"
114+
Inputs="$(MSBuildThisFileDirectory)..\BlazorDataOrchestrator.JobCreatorTemplate\**\*.*"
115+
Outputs="$(MSBuildThisFileDirectory)JobTemplate\BlazorDataOrchestrator.JobCreatorTemplate.zip">
116+
<Exec Command="pwsh -NoProfile -ExecutionPolicy Bypass -File &quot;$(MSBuildThisFileDirectory)..\..\scripts\Package-JobTemplate.ps1&quot; -SourceDir &quot;$(MSBuildThisFileDirectory)..\BlazorDataOrchestrator.JobCreatorTemplate&quot; -SlnxFile &quot;$(MSBuildThisFileDirectory)..\..\JobTemplate.slnx&quot; -OutputZip &quot;$(MSBuildThisFileDirectory)JobTemplate\BlazorDataOrchestrator.JobCreatorTemplate.zip&quot;" />
117+
</Target>
118+
```
119+
120+
Key details:
121+
- **`Inputs` / `Outputs`** — MSBuild incremental build support. The target only re-runs when template source files are newer than the zip, avoiding unnecessary work on every build.
122+
- **`BeforeTargets="BeforeBuild"`** — ensures the zip is ready before the Web project compiles and copies content to output.
123+
124+
##### 3. Ensure the zip is included as content
125+
126+
Verify that `BlazorOrchestrator.Web.csproj` already includes the zip as content (it likely does via a glob). If not, add:
127+
128+
```xml
129+
<ItemGroup>
130+
<Content Include="JobTemplate\BlazorDataOrchestrator.JobCreatorTemplate.zip">
131+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
132+
</Content>
133+
</ItemGroup>
134+
```
135+
136+
##### 4. Add the zip to `.gitignore`
137+
138+
Since the zip is now a build artifact, it should not be committed to source control:
139+
140+
```
141+
# Auto-generated job template zip
142+
src/BlazorOrchestrator.Web/JobTemplate/BlazorDataOrchestrator.JobCreatorTemplate.zip
143+
```
144+
145+
---
146+
147+
### Option B — Standalone Script Only (Simpler, Less Integrated)
148+
149+
If MSBuild integration is not desired, the same `Package-JobTemplate.ps1` script can be run manually or as a CI step. This is simpler but requires developers to remember to run it.
150+
151+
Usage:
152+
```powershell
153+
.\scripts\Package-JobTemplate.ps1 `
154+
-SourceDir "src\BlazorDataOrchestrator.JobCreatorTemplate" `
155+
-SlnxFile "JobTemplate.slnx" `
156+
-OutputZip "src\BlazorOrchestrator.Web\JobTemplate\BlazorDataOrchestrator.JobCreatorTemplate.zip"
157+
```
158+
159+
---
160+
161+
### Option C — CI-Only Automation
162+
163+
Add a step to the CI/CD pipeline (GitHub Actions / Azure DevOps) that runs the script before building the Web project. The zip remains committed in the repo for local dev but is always regenerated in CI.
164+
165+
---
166+
167+
## Recommendation
168+
169+
**Option A** is recommended because:
170+
- It is fully automatic — no manual steps, no forgotten updates.
171+
- Incremental builds prevent performance impact (the zip is only regenerated when template files change).
172+
- Works identically in local dev and CI.
173+
- The script is cross-platform compatible (PowerShell 7 / `pwsh` runs on Windows, Linux, macOS).
174+
175+
---
176+
177+
## Files Changed
178+
179+
| File | Action | Purpose |
180+
|------|--------|---------|
181+
| `scripts/Package-JobTemplate.ps1` | **Create** | Packaging script |
182+
| `src/BlazorOrchestrator.Web/BlazorOrchestrator.Web.csproj` | **Edit** | Add `PackageJobTemplate` MSBuild target |
183+
| `.gitignore` | **Edit** | Exclude the generated zip |
184+
185+
## Risks & Mitigations
186+
187+
| Risk | Mitigation |
188+
|------|------------|
189+
| `pwsh` not installed on build agent | .NET 10 SDK images include PowerShell 7. Add a check/fallback in CI. Alternatively, rewrite the script as a cross-platform shell script or pure MSBuild tasks. |
190+
| Incremental build `Inputs` glob is too broad | Refine the glob or add `Condition` checks if build times are affected. |
191+
| Zip contents diverge from expectations | Add a smoke test that extracts the zip and verifies expected files are present (e.g., `.csproj`, `Program.cs`, `.slnx`, no `bin/`). |

scripts/Package-JobTemplate.ps1

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<#
2+
.SYNOPSIS
3+
Packages the JobCreatorTemplate project into a zip for distribution.
4+
.DESCRIPTION
5+
Copies the JobCreatorTemplate source, removes build artifacts and user-specific files,
6+
includes the JobTemplate.slnx, and produces a zip ready for the Web project.
7+
.PARAMETER SourceDir
8+
Path to the BlazorDataOrchestrator.JobCreatorTemplate project directory.
9+
.PARAMETER SlnxFile
10+
Path to the JobTemplate.slnx file.
11+
.PARAMETER OutputZip
12+
Full path for the output .zip file.
13+
#>
14+
param(
15+
[Parameter(Mandatory)]
16+
[string]$SourceDir,
17+
18+
[Parameter(Mandatory)]
19+
[string]$SlnxFile,
20+
21+
[Parameter(Mandatory)]
22+
[string]$OutputZip
23+
)
24+
25+
$ErrorActionPreference = 'Stop'
26+
27+
# Create a temp staging directory
28+
$stagingDir = Join-Path ([System.IO.Path]::GetTempPath()) "JobTemplateStaging_$([guid]::NewGuid().ToString('N'))"
29+
$templateDir = Join-Path $stagingDir "BlazorDataOrchestrator.JobCreatorTemplate"
30+
31+
try {
32+
Write-Host "Packaging JobCreatorTemplate..."
33+
34+
# Copy the template project to staging
35+
Copy-Item -Path $SourceDir -Destination $templateDir -Recurse
36+
37+
# Remove directories that should not be shipped
38+
$dirsToRemove = @('bin', 'obj', 'Properties')
39+
foreach ($dir in $dirsToRemove) {
40+
$target = Join-Path $templateDir $dir
41+
if (Test-Path $target) {
42+
Remove-Item $target -Recurse -Force
43+
Write-Host " Removed $dir/"
44+
}
45+
}
46+
47+
# Remove __pycache__ directories anywhere in the tree
48+
Get-ChildItem -Path $templateDir -Directory -Filter '__pycache__' -Recurse -ErrorAction SilentlyContinue |
49+
ForEach-Object {
50+
Remove-Item $_.FullName -Recurse -Force
51+
Write-Host " Removed $($_.FullName | Split-Path -Leaf)/ (pycache)"
52+
}
53+
54+
# Remove user-specific and transient files
55+
$filesToRemove = @(
56+
'*.csproj.user',
57+
'execution_errors.log'
58+
)
59+
foreach ($pattern in $filesToRemove) {
60+
Get-ChildItem -Path $templateDir -Filter $pattern -Recurse -ErrorAction SilentlyContinue |
61+
ForEach-Object {
62+
Remove-Item $_.FullName -Force
63+
Write-Host " Removed $($_.Name)"
64+
}
65+
}
66+
67+
# Copy the .slnx file into the staging root (sibling to the project folder)
68+
Copy-Item -Path $SlnxFile -Destination $stagingDir
69+
Write-Host " Included $(Split-Path $SlnxFile -Leaf)"
70+
71+
# Ensure the output directory exists
72+
$outputDir = Split-Path $OutputZip -Parent
73+
if (-not (Test-Path $outputDir)) {
74+
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
75+
}
76+
77+
# Remove old zip if it exists
78+
if (Test-Path $OutputZip) {
79+
Remove-Item $OutputZip -Force
80+
}
81+
82+
# Create the zip
83+
Compress-Archive -Path "$stagingDir\*" -DestinationPath $OutputZip -Force
84+
85+
Write-Host "Created template zip: $OutputZip"
86+
}
87+
finally {
88+
# Clean up staging
89+
if (Test-Path $stagingDir) {
90+
Remove-Item $stagingDir -Recurse -Force
91+
}
92+
}

src/BlazorDataOrchestrator.Core/BlazorDataOrchestrator.Core.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
<PackageReference Include="Anthropic.SDK" Version="5.10.0" />
3232
<!-- Google AI packages -->
3333
<PackageReference Include="Mscc.GenerativeAI" Version="3.1.0" />
34+
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
3435
<PackageReference Include="Radzen.Blazor" Version="*" />
3536
</ItemGroup>
3637

src/BlazorDataOrchestrator.Core/JobManager.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -494,12 +494,14 @@ public async Task RunJobAsync(int jobInstanceId)
494494

495495
var evaluator = CSScript.Evaluator;
496496

497-
// Add references to all DLLs found in the package
497+
// Security: Reject packages that contain .dll files.
498+
// Only NuGet package dependencies (resolved via .nuspec) are allowed.
498499
var dlls = Directory.GetFiles(tempDir, "*.dll", SearchOption.AllDirectories);
499-
foreach (var dll in dlls)
500+
if (dlls.Length > 0)
500501
{
501-
evaluator.ReferenceAssembly(dll);
502-
await LogAsync("RunJob", $"Added reference: {Path.GetFileName(dll)}", "Debug");
502+
var dllNames = string.Join(", ", dlls.Select(System.IO.Path.GetFileName));
503+
await LogAsync("RunJob", $"Security: Rejected package containing .dll files: {dllNames}", "Error");
504+
throw new InvalidOperationException($"Package contains .dll files which are not allowed. Use NuGet packages instead. Found: {dllNames}");
503505
}
504506

505507
// Load and execute

src/BlazorDataOrchestrator.Core/Services/CodeExecutorService.cs

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -177,19 +177,16 @@ private async Task<CodeExecutionResult> ExecuteCSharpAsync(string extractedPath,
177177
}
178178
}
179179

180-
// Add references to DLLs found in the package
180+
// Security: Reject packages that contain .dll files.
181+
// Only NuGet package dependencies (resolved via .nuspec) are allowed.
181182
var dlls = Directory.GetFiles(extractedPath, "*.dll", SearchOption.AllDirectories);
182-
foreach (var dll in dlls)
183+
if (dlls.Length > 0)
183184
{
184-
try
185-
{
186-
evaluator.ReferenceAssembly(dll);
187-
result.Logs.Add($"Added reference: {Path.GetFileName(dll)}");
188-
}
189-
catch
190-
{
191-
// Ignore DLLs that can't be loaded
192-
}
185+
var dllNames = string.Join(", ", dlls.Select(Path.GetFileName));
186+
result.Success = false;
187+
result.ErrorMessage = $"Package contains .dll files which are not allowed. Use NuGet packages instead. Found: {dllNames}";
188+
result.Logs.Add($"Security: Rejected package containing .dll files: {dllNames}");
189+
return result;
193190
}
194191

195192
// Add common references
@@ -234,24 +231,6 @@ private async Task<CodeExecutionResult> ExecuteCSharpAsync(string extractedPath,
234231
}
235232
}
236233

237-
// Also load DLLs from the package
238-
foreach (var dll in dlls)
239-
{
240-
try
241-
{
242-
var loadedAsm = Assembly.LoadFrom(dll);
243-
var asmName = loadedAsm.GetName().Name;
244-
if (asmName != null && !loadedAssemblies.ContainsKey(asmName))
245-
{
246-
loadedAssemblies[asmName] = loadedAsm;
247-
}
248-
}
249-
catch
250-
{
251-
// Ignore DLLs that can't be loaded
252-
}
253-
}
254-
255234
result.Logs.Add($"Pre-loaded {loadedAssemblies.Count} assemblies for runtime resolution.");
256235

257236
// Set up assembly resolve handler for the compiled script

src/BlazorDataOrchestrator.Core/Services/NuGetPackageBuilderService.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,21 @@ public async Task<PackageBuildResult> BuildPackageAsync(PackageBuildConfiguratio
125125
var csharpFolder = Path.Combine(config.CodeRootPath, "CodeCSharp");
126126
var pythonFolder = Path.Combine(config.CodeRootPath, "CodePython");
127127

128+
// Security: Reject source directories that contain .dll files.
129+
// Users must use NuGet package dependencies declared in .nuspec instead.
130+
if (Directory.Exists(config.CodeRootPath))
131+
{
132+
var dllFiles = Directory.GetFiles(config.CodeRootPath, "*.dll", SearchOption.AllDirectories);
133+
if (dllFiles.Length > 0)
134+
{
135+
var dllNames = string.Join(", ", dllFiles.Select(Path.GetFileName));
136+
result.Success = false;
137+
result.ErrorMessage = $"Source directory contains .dll files which are not allowed in packages. Use NuGet packages instead. Found: {dllNames}";
138+
result.Logs.Add($"Security: Rejected build due to .dll files: {dllNames}");
139+
return result;
140+
}
141+
}
142+
128143
// Copy JSON files from the root Code folder
129144
if (Directory.Exists(config.CodeRootPath))
130145
{

0 commit comments

Comments
 (0)