Skip to content

Commit adbe7a4

Browse files
authored
Support for dotnet framework. Dependency tree for csproj files (#1119)
* Support for dotnet framework. Dependency tree for csproj files Signed-off-by: Prabhu Subramanian <[email protected]> * Fixes dosai invocation on darwin Signed-off-by: Prabhu Subramanian <[email protected]> * Fixes dosai invocation on darwin Signed-off-by: Prabhu Subramanian <[email protected]> * Invoke nuget restore Signed-off-by: Prabhu Subramanian <[email protected]> * Tweaks Signed-off-by: Prabhu Subramanian <[email protected]> * Adds .net framework sample for repo tests Signed-off-by: Prabhu Subramanian <[email protected]> * Dependency tree from nuspec Signed-off-by: Prabhu Subramanian <[email protected]> * Dependency tree from nuspec Signed-off-by: Prabhu Subramanian <[email protected]> * Parse csproj files with version map Signed-off-by: Prabhu Subramanian <[email protected]> * Another repotest for csproj level tree Signed-off-by: Prabhu Subramanian <[email protected]> * Unit test Signed-off-by: Prabhu Subramanian <[email protected]> * Set required scope based on occurrence evidence for dotnet Signed-off-by: Prabhu Subramanian <[email protected]> --------- Signed-off-by: Prabhu Subramanian <[email protected]>
1 parent 87ed9c9 commit adbe7a4

17 files changed

+2574
-73
lines changed

.github/workflows/repotests.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,16 @@ jobs:
166166
repository: 'fsprojects/FAKE'
167167
ref: '6.0.0'
168168
path: 'repotests/dotnet-paket'
169+
- uses: actions/checkout@v4
170+
with:
171+
repository: 'timheuer/SimpleFrameworkApp'
172+
ref: 'master'
173+
path: 'repotests/SimpleFrameworkApp'
174+
- uses: actions/checkout@v4
175+
with:
176+
repository: 'chabbasaad/Reporting-Windows-Application'
177+
ref: 'master'
178+
path: 'repotests/Reporting-Windows-Application'
169179
- uses: actions/checkout@v4
170180
with:
171181
repository: 'appthreat/blint'
@@ -312,11 +322,16 @@ jobs:
312322
shell: bash
313323
- name: repotests dotnet-paket
314324
run: |
315-
bin/cdxgen.js -p -r -t dotnet repotests/dotnet-paket -o bomresults/bom-dotnet-paket.json --validate
325+
bin/cdxgen.js -p -r -t dotnet repotests/dotnet-paket -o bomresults/bom-dotnet-paket.json --deep
316326
FETCH_LICENSE=true bin/cdxgen.js -p -r -t dotnet repotests/dotnet-paket -o bomresults/bom-dotnet-paket-2.json --validate
317327
bin/cdxgen.js -p -r -t dotnet repotests/dotnet-podcasts -o bomresults/bom-dotnet-podcasts.json --profile research --export-proto
318328
bin/cdxgen.js -p -r -t dotnet repotests/react-native-windows -o bomresults/bom-react-native-windows.json
319329
shell: bash
330+
- name: repotests SimpleFrameworkApp
331+
run: |
332+
bin/cdxgen.js -p -r -t dotnet-framework repotests/SimpleFrameworkApp -o bomresults/bom-dotnet-framework.json
333+
bin/cdxgen.js -p -r -t dotnet-framework repotests/Reporting-Windows-Application -o bomresults/bom-dotnet-framework-reporting.json --deep
334+
shell: bash
320335
- name: repotests blint
321336
run: |
322337
bin/cdxgen.js -p -t python repotests/blint -o bomresults/bom-blint.json

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
20
1+
22

binary.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,14 +192,10 @@ if (existsSync(join(CDXGEN_PLUGINS_DIR, "osquery"))) {
192192
}
193193
let DOSAI_BIN = null;
194194
if (existsSync(join(CDXGEN_PLUGINS_DIR, "dosai"))) {
195-
let platformToUse = platform;
196-
if (platform === "darwin") {
197-
platformToUse = "osx";
198-
}
199195
DOSAI_BIN = join(
200196
CDXGEN_PLUGINS_DIR,
201197
"dosai",
202-
`dosai-${platformToUse}-${arch}${extn}`,
198+
`dosai-${platform}-${arch}${extn}`,
203199
);
204200
} else if (process.env.DOSAI_CMD) {
205201
DOSAI_BIN = process.env.DOSAI_CMD;

deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cyclonedx/cdxgen",
3-
"version": "10.5.1",
3+
"version": "10.5.2",
44
"exports": "./index.js",
55
"compilerOptions": {
66
"allowJs": true,

docs/PROJECT_TYPES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ _Note: there are multiple project types / aliases that will produce the same out
2323
| Golang | `go`, `golang` | `binary`, `go.mod`, `go.sum`, `Gopkg.lock` | Yes except binary ||
2424
| Rust | `rust`, `rust-lang`, `cargo` | `binary`, `Cargo.toml`, `Cargo.lock` | Only for `Cargo.lock` | - |
2525
| Ruby | `ruby`, `gems` | `Gemfile.lock`, `gemspec` | Only for `Gemfile.lock` | - |
26-
| .NET (#C) | `csharp`, `netcore`, `dotnet`, `vb` | `.csproj`, `.vbproj`, `.fsproj`, `packages.config`, `project.assets.json` [3], `packages.lock.json`, `.nupkg`, `paket.lock`, `binary` | Only for `project.assets.json`, `packages.lock.json`, `paket.lock` | - |
26+
| .NET (#C) | `csharp`, `netcore`, `dotnet`, `vb`, `dotnet-framework` | `.csproj`, `.vbproj`, `.fsproj`, `packages.config`, `project.assets.json` [3], `packages.lock.json`, `.nupkg`, `paket.lock`, `binary` | Only for `project.assets.json`, `packages.lock.json`, `paket.lock` | - |
2727
| Dart | `dart`, `flutter`, `pub` | `pubspec.lock`, `pubspec.yaml` | Only for `pubspec.lock` | - |
2828
| Haskell | `haskell`, `hackage`, `cabal` | `cabal.project.freeze` | Yes | |
2929
| Elixir | `elixir`, `hex`, `mix` | `mix.lock` | Yes | - |

index.js

Lines changed: 103 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4652,7 +4652,7 @@ export async function createCsharpBom(path, options) {
46524652
if (options.lifecycle === "post-build") {
46534653
return createBinaryBom(path, options);
46544654
}
4655-
const parentComponent = createDefaultParentComponent(path, "nuget", options);
4655+
let parentComponent = createDefaultParentComponent(path, "nuget", options);
46564656
const slnFiles = getAllFiles(
46574657
path,
46584658
`${options.multiProject ? "**/" : ""}*.sln`,
@@ -4682,7 +4682,7 @@ export async function createCsharpBom(path, options) {
46824682
`${options.multiProject ? "**/" : ""}project.assets.json`,
46834683
options,
46844684
);
4685-
const pkgLockFiles = getAllFiles(
4685+
let pkgLockFiles = getAllFiles(
46864686
path,
46874687
`${options.multiProject ? "**/" : ""}packages.lock.json`,
46884688
options,
@@ -4692,12 +4692,12 @@ export async function createCsharpBom(path, options) {
46924692
`${options.multiProject ? "**/" : ""}paket.lock`,
46934693
options,
46944694
);
4695-
const nupkgFiles = getAllFiles(
4695+
let nupkgFiles = getAllFiles(
46964696
path,
46974697
`${options.multiProject ? "**/" : ""}*.nupkg`,
46984698
options,
46994699
);
4700-
// Support for automatic restore
4700+
// Support for automatic restore for .Net projects
47014701
if (
47024702
options.installDeps &&
47034703
!projAssetsFiles.length &&
@@ -4706,47 +4706,90 @@ export async function createCsharpBom(path, options) {
47064706
) {
47074707
const filesToRestore = slnFiles.concat(csProjFiles);
47084708
for (const f of filesToRestore) {
4709+
const buildCmd =
4710+
options.projectType === "dotnet-framework" ? "nuget" : "dotnet";
47094711
if (DEBUG_MODE) {
47104712
const basePath = dirname(f);
4711-
console.log("Executing 'dotnet restore' in", basePath);
4713+
console.log(`Executing '${buildCmd} restore' in ${basePath}`);
47124714
}
47134715
const result = spawnSync(
4714-
"dotnet",
4715-
["restore", "--force", "--ignore-failed-sources", f],
4716+
buildCmd,
4717+
options.projectType === "dotnet-framework"
4718+
? [
4719+
"restore",
4720+
"-NonInteractive",
4721+
"-Recursive",
4722+
"-PackageSaveMode",
4723+
"nuspec;nupkg",
4724+
"-Verbosity",
4725+
"quiet",
4726+
]
4727+
: ["restore", "--force", "--ignore-failed-sources", f],
47164728
{
47174729
cwd: path,
47184730
encoding: "utf-8",
47194731
},
47204732
);
47214733
if (DEBUG_MODE && (result.status !== 0 || result.error)) {
47224734
console.error(
4723-
"Restore has failed. Check if dotnet is installed and available in PATH.",
4735+
`Restore has failed. Check if ${buildCmd} is installed and available in PATH.`,
47244736
);
47254737
console.log(
47264738
"Authenticate with any private registries such as Azure Artifacts feed before running cdxgen.",
47274739
);
4740+
console.log(
4741+
"Alternatively, try using the unofficial `ghcr.io/appthreat/cdxgen-dotnet6:v10` container image, which bundles nuget (mono) and a range of dotnet SDKs.",
4742+
);
47284743
console.log(result.stderr);
47294744
options.failOnError && process.exit(1);
47304745
}
47314746
}
4732-
// Collect the assets file generated from restore
4747+
// Collect the assets, lock, and nupkg files generated from restore
47334748
projAssetsFiles = getAllFiles(
47344749
path,
47354750
`${options.multiProject ? "**/" : ""}project.assets.json`,
47364751
options,
47374752
);
4753+
pkgLockFiles = getAllFiles(
4754+
path,
4755+
`${options.multiProject ? "**/" : ""}packages.lock.json`,
4756+
options,
4757+
);
4758+
nupkgFiles = getAllFiles(
4759+
path,
4760+
`${options.multiProject ? "**/" : ""}*.nupkg`,
4761+
options,
4762+
);
47384763
}
47394764
let pkgList = [];
4765+
const parentDependsOn = new Set();
47404766
if (nupkgFiles.length && projAssetsFiles.length === 0) {
47414767
manifestFiles = manifestFiles.concat(nupkgFiles);
4768+
// When parsing nupkg files, only version ranges will be specified under dependencies
4769+
// To resolve the version, we need to track the mapping between name and resolved versions here
4770+
let dependenciesMap = {};
4771+
const pkgNameVersions = {};
47424772
for (const nf of nupkgFiles) {
47434773
if (DEBUG_MODE) {
47444774
console.log(`Parsing ${nf}`);
47454775
}
4746-
const dlist = await parseNupkg(nf);
4747-
if (dlist?.length) {
4748-
pkgList = pkgList.concat(dlist);
4776+
const retMap = await parseNupkg(nf);
4777+
if (retMap?.pkgList.length) {
4778+
pkgList = pkgList.concat(retMap.pkgList);
4779+
for (const d of retMap.pkgList) {
4780+
parentDependsOn.add(d["bom-ref"]);
4781+
pkgNameVersions[d.name] = d.version;
4782+
}
47494783
}
4784+
if (retMap?.dependenciesMap) {
4785+
dependenciesMap = { ...dependenciesMap, ...retMap.dependenciesMap };
4786+
}
4787+
} // end for
4788+
for (const k of Object.keys(dependenciesMap)) {
4789+
const dependsOn = dependenciesMap[k].map(
4790+
(p) => `pkg:nuget/${p}@${pkgNameVersions[p] || "latest"}`,
4791+
);
4792+
dependencies.push({ ref: k, dependsOn: dependsOn.sort() });
47504793
}
47514794
}
47524795
// project.assets.json parsing
@@ -4780,7 +4823,7 @@ export async function createCsharpBom(path, options) {
47804823
"2. Use the environment variable `DOTNET_ROLL_FORWARD` to roll forward to a closest available SDK such as .Net core or dotnet 6.",
47814824
);
47824825
console.log(
4783-
"3. If the project uses the legacy .Net Framework 4.6/4.7, it might require Windows operating system.",
4826+
"3. If the project uses the legacy .Net Framework 4.6/4.7/4.8, it might require execution on Windows.",
47844827
);
47854828
console.log(
47864829
"Alternatively, try using the unofficial `ghcr.io/appthreat/cdxgen-dotnet:v10` container image, which bundles a range of dotnet SDKs.",
@@ -4789,7 +4832,6 @@ export async function createCsharpBom(path, options) {
47894832
}
47904833
} else if (pkgLockFiles.length) {
47914834
manifestFiles = manifestFiles.concat(pkgLockFiles);
4792-
const parentDependsOn = new Set();
47934835
// packages.lock.json from nuget
47944836
for (const af of pkgLockFiles) {
47954837
if (DEBUG_MODE) {
@@ -4814,12 +4856,6 @@ export async function createCsharpBom(path, options) {
48144856
}
48154857
}
48164858
}
4817-
if (parentDependsOn.size) {
4818-
dependencies.splice(0, 0, {
4819-
ref: parentComponent["bom-ref"],
4820-
dependsOn: Array.from(parentDependsOn),
4821-
});
4822-
}
48234859
} else if (pkgConfigFiles.length) {
48244860
manifestFiles = manifestFiles.concat(pkgConfigFiles);
48254861
// packages.config parsing
@@ -4832,9 +4868,12 @@ export async function createCsharpBom(path, options) {
48324868
if (pkgData.charCodeAt(0) === 0xfeff) {
48334869
pkgData = pkgData.slice(1);
48344870
}
4835-
const dlist = parseCsPkgData(pkgData);
4871+
const dlist = parseCsPkgData(pkgData, f);
48364872
if (dlist?.length) {
48374873
pkgList = pkgList.concat(dlist);
4874+
for (const d of dlist) {
4875+
parentDependsOn.add(d["bom-ref"]);
4876+
}
48384877
}
48394878
}
48404879
}
@@ -4857,8 +4896,19 @@ export async function createCsharpBom(path, options) {
48574896
}
48584897
}
48594898
}
4860-
if (!pkgList.length && csProjFiles.length) {
4899+
if (
4900+
options.projectType === "dotnet-framework" ||
4901+
(!pkgList.length && csProjFiles.length && !nupkgFiles.length)
4902+
) {
48614903
manifestFiles = manifestFiles.concat(csProjFiles);
4904+
// Parsing csproj is quite error prone. Some project files may not have versions specified
4905+
// To work around this, we make use of the version from the existing list
4906+
const pkgNameVersions = {};
4907+
for (const p of pkgList) {
4908+
if (p.version) {
4909+
pkgNameVersions[p.name] = p.version;
4910+
}
4911+
}
48624912
// .csproj parsing
48634913
for (const f of csProjFiles) {
48644914
if (DEBUG_MODE) {
@@ -4869,12 +4919,31 @@ export async function createCsharpBom(path, options) {
48694919
if (csProjData.charCodeAt(0) === 0xfeff) {
48704920
csProjData = csProjData.slice(1);
48714921
}
4872-
const dlist = parseCsProjData(csProjData, f);
4873-
if (dlist?.length) {
4874-
pkgList = pkgList.concat(dlist);
4922+
const retMap = parseCsProjData(csProjData, f, pkgNameVersions);
4923+
if (retMap?.parentComponent) {
4924+
// If there are multiple project files, track the parent components using nested components
4925+
if (csProjFiles.length > 1) {
4926+
if (!parentComponent.components) {
4927+
parentComponent.components = [];
4928+
}
4929+
parentComponent.components.push(retMap.parentComponent);
4930+
} else {
4931+
// There is only one project file. Make it the parent.
4932+
parentComponent = retMap.parentComponent;
4933+
}
4934+
}
4935+
if (retMap?.pkgList.length) {
4936+
pkgList = pkgList.concat(retMap.pkgList);
4937+
}
4938+
if (retMap.dependencies?.length) {
4939+
dependencies = mergeDependencies(
4940+
dependencies,
4941+
retMap.dependencies,
4942+
parentComponent,
4943+
);
48754944
}
48764945
}
4877-
if (pkgList.length) {
4946+
if (pkgList.length && options.projectType !== "dotnet-framework") {
48784947
console.log(
48794948
`Found ${pkgList.length} components by parsing the ${csProjFiles.length} csproj files. The resulting SBOM will be incomplete.`,
48804949
);
@@ -4900,6 +4969,13 @@ export async function createCsharpBom(path, options) {
49004969
pkgList = addEvidenceForDotnet(pkgList, slicesFile, options);
49014970
}
49024971
}
4972+
// Parent dependency tree
4973+
if (parentDependsOn.size && parentComponent && parentComponent["bom-ref"]) {
4974+
dependencies.splice(0, 0, {
4975+
ref: parentComponent["bom-ref"],
4976+
dependsOn: Array.from(parentDependsOn),
4977+
});
4978+
}
49034979
if (FETCH_LICENSE) {
49044980
const retMap = await getNugetMetadata(pkgList, dependencies);
49054981
if (retMap.dependencies?.length) {
@@ -6036,6 +6112,7 @@ export async function createBom(path, options) {
60366112
case "csharp":
60376113
case "netcore":
60386114
case "dotnet":
6115+
case "dotnet-framework":
60396116
case "vb":
60406117
return await createCsharpBom(path, options);
60416118
case "dart":

jsr.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cyclonedx/cdxgen",
3-
"version": "10.5.1",
3+
"version": "10.5.2",
44
"exports": "./index.js",
55
"include": ["*.js", "bin/**", "data/**", "types/**"],
66
"exclude": ["test/", "docs/", "contrib/", "ci/", "tools_config/"]

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cyclonedx/cdxgen",
3-
"version": "10.5.1",
3+
"version": "10.5.2",
44
"description": "Creates CycloneDX Software Bill of Materials (SBOM) from source or container image",
55
"homepage": "http://github.com/cyclonedx/cdxgen",
66
"author": "Prabhu Subramanian <[email protected]>",
@@ -28,7 +28,7 @@
2828
"url": "https://about.me/stevespringett"
2929
},
3030
{
31-
"name": "Prabhu S",
31+
"name": "Prabhu Subramanian",
3232
"url": "https://github.com/prabhu"
3333
},
3434
{

0 commit comments

Comments
 (0)