diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml index b779b53..56756c6 100644 --- a/.github/workflows/keyfactor-starter-workflow.yml +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -1,33 +1,29 @@ -name: Starter Workflow -on: [workflow_dispatch, push, pull_request] +name: Keyfactor Bootstrap Workflow -jobs: - call-create-github-release-workflow: - uses: Keyfactor/actions/.github/workflows/github-release.yml@main - - call-assign-from-json-workflow: - uses: Keyfactor/actions/.github/workflows/assign-env-from-json.yml@main +on: + workflow_dispatch: + pull_request: + types: [opened, closed, synchronize, edited, reopened] + push: + create: + branches: + - 'release-*.*' - call-dotnet-build-and-release-workflow: - needs: [call-create-github-release-workflow, call-assign-from-json-workflow] - uses: Keyfactor/actions/.github/workflows/dotnet-build-and-release.yml@main +jobs: + call-starter-workflow: + uses: keyfactor/actions/.github/workflows/starter.yml@v4 + permissions: + contents: write # Explicitly grant write permission with: - release_version: ${{ needs.call-create-github-release-workflow.outputs.release_version }} - release_url: ${{ needs.call-create-github-release-workflow.outputs.release_url }} - release_dir: ${{ needs.call-assign-from-json-workflow.outputs.release_dir }} - - secrets: - token: ${{ secrets.PRIVATE_PACKAGE_ACCESS }} - - call-generate-readme-workflow: - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' - uses: Keyfactor/actions/.github/workflows/generate-readme.yml@main + command_token_url: ${{ vars.COMMAND_TOKEN_URL }} + command_hostname: ${{ vars.COMMAND_HOSTNAME }} + command_base_api_path: ${{ vars.COMMAND_API_PATH }} secrets: - token: ${{ secrets.APPROVE_README_PUSH }} - - call-update-catalog-workflow: - needs: call-assign-from-json-workflow - if: needs.call-assign-from-json-workflow.outputs.update_catalog == 'True' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') - uses: Keyfactor/actions/.github/workflows/update-catalog.yml@main - secrets: - token: ${{ secrets.SDK_SYNC_PAT }} + token: ${{ secrets.V2BUILDTOKEN}} + gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} + gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} + scan_token: ${{ secrets.SAST_TOKEN }} + entra_username: ${{ secrets.DOCTOOL_ENTRA_USERNAME }} + entra_password: ${{ secrets.DOCTOOL_ENTRA_PASSWD }} + command_client_id: ${{ secrets.COMMAND_CLIENT_ID }} + command_client_secret: ${{ secrets.COMMAND_CLIENT_SECRET }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index dfcfd56..44e7d0a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,350 +1,359 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ +/digicert-metadata-sync/digicert-metadata-sync/App - Copy.config +/digicert-metadata-sync/digicert-metadata-sync/App - Blank.config +/digicert-metadata-sync/digicert-metadata-sync/manualfields - Copy.json +/digicert-metadata-sync/config/config.json +/digicert-metadata-sync/config/config.json +/digicert-metadata-sync.zip +/digicert-metadata-sync/config/config-mk.json +/digicert-metadata-sync/config/fields-mk.json +/digicert-metadata-sync/config/fields.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 6804b4e..6588d17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,42 @@ -Version 2.1.0 - - Added a system that gathers all non-Keyfactor friendly characters and allows the user to configure an alternative. - Added pagination based batch processing, memory consumption has been drastically reduced. - -Version 2.0.3 - - Added a setting to enable or disable syncing deactivated custom fields from DigiCert. - -Version 2.0.2 - - Fixed issue with additional_emails field not syncing. - Added independent logging via NLog. - -Version 2.0.1 - - Fixed issue with no input for either custom or manual fields leading to a crash. - Fixed issue with data for imported DigiCert fields renamed with a replacement character not syncing back to DigiCert. - Fixed possible crash caused by importing DigiCert custom fields with "Anything" data type. - -Version 2.0.0 - - Added ability to sync custom fields from Keyfactor to DigiCert. - Tool now requires command line argument to specify sync direction: "dctokf" for DigiCert to Keyfactor and "kftodc" for Keyfactor to DigiCert. - New DigiCert API Key with restrictions set to "None" in DigiCert config required to perform sync from Keyfactor to Digicert. - -Version 1.0 - - Initial Release +Version 3.0.0 + + ⚠️ Important Notice + **Configuration files and their location have changed since version 2.1.0** Please review the documentation and see the new stock configuration files for guidance on how to set up the tool. + The configuration files will need to be placed in the `config` subdirectory for use with the tool. + + Rewrote the sync engine to improve performance and resilience. + New retry logic now automatically backs off when rate limits are hit on DigiCert. + Config system now uses json file instead of xml, and all config files are aggregated in the config directory. + Fixed issue with new Keyfactor versions being broken due to lack of DisplayOrder in the metadata fields API. + Fixed issue with email fields not syncing properly. + Implemented a new logging system using NLog, with log files stored in the logs directory, and an nlog.config file. + +Version 2.1.0 + + Added a system that gathers all non-Keyfactor friendly characters and allows the user to configure an alternative. + Added pagination based batch processing, memory consumption has been drastically reduced. + +Version 2.0.3 + + Added a setting to enable or disable syncing deactivated custom fields from DigiCert. + +Version 2.0.2 + + Fixed issue with additional_emails field not syncing. + Added independent logging via NLog. + +Version 2.0.1 + + Fixed issue with no input for either custom or manual fields leading to a crash. + Fixed issue with data for imported DigiCert fields renamed with a replacement character not syncing back to DigiCert. + Fixed possible crash caused by importing DigiCert custom fields with "Anything" data type. + +Version 2.0.0 + + Added ability to sync custom fields from Keyfactor to DigiCert. + Tool now requires command line argument to specify sync direction: "dctokf" for DigiCert to Keyfactor and "kftodc" for Keyfactor to DigiCert. + New DigiCert API Key with restrictions set to "None" in DigiCert config required to perform sync from Keyfactor to Digicert. + +Version 1.0 + + Initial Release \ No newline at end of file diff --git a/DigicertMetadataSync.sln b/DigicertMetadataSync.sln index bd6ebc8..d97238b 100644 --- a/DigicertMetadataSync.sln +++ b/DigicertMetadataSync.sln @@ -1,9 +1,18 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.2.32616.157 +VisualStudioVersion = 17.14.36414.22 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigicertMetadataSync", "digicert-metadata-sync\DigicertMetadataSync.csproj", "{AF8D8189-8B56-4F4A-947E-3DD9066B3227}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigicertMetadataSync", "digicert-metadata-sync\DigicertMetadataSync.csproj", "{A3E989D8-5262-9630-0136-1FD5D33641CD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" + ProjectSection(SolutionItems) = preProject + CHANGELOG.md = CHANGELOG.md + docsource\content.md = docsource\content.md + integration-manifest.json = integration-manifest.json + .github\workflows\keyfactor-starter-workflow.yml = .github\workflows\keyfactor-starter-workflow.yml + README.md = README.md + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,15 +20,15 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {AF8D8189-8B56-4F4A-947E-3DD9066B3227}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AF8D8189-8B56-4F4A-947E-3DD9066B3227}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AF8D8189-8B56-4F4A-947E-3DD9066B3227}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AF8D8189-8B56-4F4A-947E-3DD9066B3227}.Release|Any CPU.Build.0 = Release|Any CPU + {A3E989D8-5262-9630-0136-1FD5D33641CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A3E989D8-5262-9630-0136-1FD5D33641CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A3E989D8-5262-9630-0136-1FD5D33641CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A3E989D8-5262-9630-0136-1FD5D33641CD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {88551ADC-3044-4BCF-9AA2-4A919D6D9574} + SolutionGuid = {A235DCC4-4D23-45CD-BA8E-BF5052AA1203} EndGlobalSection EndGlobal diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 261eeb9..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/README.md b/README.md index 72f6b2a..538579c 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,265 @@ -# Digicert Metadata Sync +

+ DigiCert Metadata Sync +

-A tool to automatically synchronize metadata fields and their content from DigiCert to Keyfactor. This utility is indented to be used in conjunction with the Digicert AnyGateway and adds to the information already synchronized by the gateway. +

+ +Integration Status: production +Release +Issues +GitHub Downloads (all assets, all releases) +

-#### Integration status: Production - Ready for use in production environments. +

+ + + Support + + · + + License + + · + + Related Integrations + +

+## Support +The DigiCert Metadata Sync is open source and there is **no SLA**. Keyfactor will address issues as resources become available. Keyfactor customers may request escalation by opening up a support ticket through their Keyfactor representative. +> To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab. -## Support for Digicert Metadata Sync -Digicert Metadata Sync is open source and there is **no SLA** for this tool/library/client. Keyfactor will address issues as resources become available. Keyfactor customers may request escalation by opening up a support ticket through their Keyfactor representative. - -###### To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab. +## Overview +### ⚠️ Important Notice +**Configuration files and their location have changed since version 2.1.0** Please review the documentation and see the new stock configuration files for guidance on how to set up the tool. The configuration files will need to be placed in the `config` subdirectory for use with the tool. +This tool automates the synchronization of metadata fields between **DigiCert CertCentral** and **Keyfactor Command**. It performs two primary operations: +1. **DCtoKF** - Synchronizes *manual fields* and *custom fields* from DigiCert into Keyfactor. +2. **KFtoDC** - Synchronizes *custom fields* from Keyfactor back into DigiCert. +> **Notes** +> +> * **ManualFields** are values present in DigiCert's *Order Info* JSON and are mapped by dot path (e.g., `organization_contact.email`). Manual field data is available **only** for DigiCert -> Keyfactor sync. +> * **CustomFields** are DigiCert CertCentral custom fields and can be synchronized in **both** directions (DigiCert <--> Keyfactor). +> * The list of available **manual** fields is derived from the DigiCert *Order Info* API. See: [DigiCert Order Info API response](https://dev.digicert.com/en/certcentral-apis/services-api/orders/order-info.html) +> * Certificates must already exist in Keyfactor; this tool does **not** import certificates. -## Overview -This tool primarily sets up metadata fields in Keyfactor for the custom metadata fields in DigiCert, which are named as such, but can also setup metadata fields in Keyfactor for non-custom fields available in DigiCert and unavailable in Keyfactor by default, such as the Digicert Cert ID and the Organization contact. These fields are referred to as manual fields in the context of this tool. After setting up these fields, the tool proceeds to update the contents of these fields. This tool only adds metadata to certificates that have already been imported into Keyfactor. Additionally, this tool requires a properly installed and functioning AnyGateway configured to work with Keyfactor and Digicert. The latest update allows for syncronization of custom field contents from Keyfactor to DigiCert. New fields are created in Keyfactor and DigiCert to accomodate for this. +--- ## Installation and Usage -The tool comes as a Windows executable. The tool performs synchronization each time its run. For the tool to run automatically, it needs to be added as a scheduled process using Windows. The advised interval for running it is once per week. The files DigicertMetadataSync.dll.config and manualfields.json need to be present in the same directory as the tool for it to run correctly. The specific location from which the tool is ran does not matter, but it needs to have access to both the Keyfactor API endpoint as well as Digicert, and appropriate permissions for access to the configuration files. -An explanation for the settings found in these files is given below. -## Command Line Arguments -One of these two arguments needs to be used for the tool to run. -- "kftodc" -Syncronizes the contents of custom fields listed in manualfields.json from Keyfactor to DigiCert. If the fields in manualfields.json do not exist in Keyfactor or DigiCert, they are created first. Example: ```.\DigicertMetadataSync.exe kftodc``` -- "dctokf" -Syncronizes the contents of both custom and non-custom fields from DigiCert to Keyfactor. The fields are listed in manualfields.json, and are created if necessary. -Example: ```.\DigicertMetadataSync.exe dctokf``` +### Prerequisites + +- .NET **9** (or newer) runtime. +- DigiCert **API key** with **API key restrictions (optional)** set to **None** when creating the key in CertCentral. +- A Keyfactor account with API access and permission to create/edit metadata fields and modify certificates. +- The following files in the **`config`** subdirectory: + - `config.json` + - `fields.json` + - `bannedcharacters.json` (auto-generated on first run if needed) + +Additional notes: + +- Designed for **Keyfactor 25.1**; tested compatible with older versions. +- The tool communicates directly with **Keyfactor Command API** and **DigiCert** - no Keyfactor Gateway dependency. +- Independent logging: logs are written to a local `logs/` folder next to the executable. + +### Running the Tool + +From the tool directory, open PowerShell and run: + +```powershell +./DigicertMetadataSync.exe dctokf +``` + +or + +```powershell +./DigicertMetadataSync.exe kftodc +``` + +> **Tip:** The tool performs one sync in the specified direction and then exits. Schedule it (e.g., with Windows Task Scheduler) for recurring syncs. + +--- + +## Command Line Modes + +One of the following modes must be supplied as the **first (and only) argument**: + +- `dctokf` + Synchronizes **manual** and **custom** fields **from DigiCert to Keyfactor**. + - Reads mappings from `fields.json` for manual fields. + - If `importAllCustomDigicertFields` is **true**, imports *all* DigiCert custom fields; otherwise uses only those listed under `CustomFields` in `fields.json`. + - Ensures required metadata fields exist in Keyfactor, creating missing ones. + - Locates DigiCert-issued certs in Keyfactor (by Issuer DN filter). + - Updates Keyfactor metadata with coerced values. + +- `kftodc` + Synchronizes **custom** fields **from Keyfactor to DigiCert**. + - Reads mappings from `fields.json` for custom fields. + - Ensures required metadata fields exist in Keyfactor. + - If `createMissingFieldsInDigicert` is **true** and `importAllCustomDigicertFields` is **false**, attempts to create missing DigiCert custom fields (limited by DigiCert API capabilities). + - Locates DigiCert-issued certs in Keyfactor (by Issuer DN filter). + - Updates DigiCert custom field values with coerced data types. + +> **Important:** Run `dctokf` at least once before running `kftodc` so Keyfactor metadata fields exist and have been normalized. + +--- ## Settings -The settings currently present in these files are shown as an example and need to be configured for your specific situation. -### DigicertMetadataSync.dll.config settings -- DigicertAPIKey -Standard DigiCert API access key. -- DigicertAPIKeyTopPerm -DigiCert API access key with restrictions set to "None" - required for sync from Keyfactor to DigiCert. -- KeyfactorDomainAndUser -Same credential as used when logging into Keyfactor Command. A different set of credentials can be used provided they have adequate access permissions. -- KeyfactorPassword -Password for the account used in the KeyfactorDomainAndUser field. -- KeyfactorCertSearchReturnLimit -This specifies the number of certs the tool will expect to receive from Keyfactor Command. Can be set to an arbitrarily large number for unlimited or to a smaller number for testing. -- KeyfactorAPIEndpoint -This should include the Keyfactor API endpoint, of the format https://domain.com/keyfactorapi/ -- KeyfactorDigicertIssuedCertQueryTerm -This should include the common prefix all DigiCert certs have in your Keyfactor instance. For example, "DigiCert" -- ImportAllCustomDigicertFields -This setting enables the tool to import all of the custom metadata fields included in DigiCert and sync all of their data. - -During the first run, the tool will scan the custom fields it will be importing for characters that are not supported in Keyfactor Metadata field names. -Each unsupported character will be shown in a file named "replacechar.json" and its replacement can be selected. If the values in the file are not populated, the tool will not run a second time. -- ImportDataForDeactivatedDigiCertFields -If this is enabled, custom metadata fields that were deactivated in DigiCert will also be synced, and the data stored in these fields in certificates will be too. - -### replacechar.json settings -This file is populated during the first run of the tool if the ImportAllCustomDigicertFields setting is toggled. -The only text that needs replacing is shown as "null", and can be filled with any alphanumeric string. The "_" and "-" characters are also supported. - - -### manualfields.json settings -This file is used to specify which metadata fields should be synced up. - -The "ManualFields" section is used to specify the non custom fields to import into Keyfactor. - -The "CustomFields" section is used to specify which of the custom metadata fields in DigiCert should be imported into Keyfactor. - -- DigicertFieldName -For "ManualFields", this should specify the location and name of the field in the json returned from the DigiCert API following a certificate order query. If the field is not at the top level, the input should be delimited using a "." character: "organization_contact.email". The structure of the json the API returns can be viewed here: https://dev.digicert.com/services-api/orders/order-info/ -For "CustomFields", this should be the label of the custom metadata field as listed in DigiCert. - -- KeyfactorMetadataFieldName -This is the string that will be used as the field name in Keyfactor. -For "ManualFields", this needs to be configured. -For "CustomFields", if left blank, will use the same name as the same string as the DigicertFieldName, provided it has no spaces. - -- KeyfactorDescription -This is the string that will be setup as the field description in Keyfactor. - -- KeyfactorDataType -The datatype the field will use in Keyfactor. Currently accepted types are Int and String. - -- KeyfactorDataType -String to be input into Keyfactor as the metadata field hint. - -- KeyfactorAllowAPI -Allows API management of this metadata field in Keyfactor. Should be set to true for continuous synchronization with this tool. - -### Logging -Logging functionality can be configured via entering either "Debug" or "Trace" into the value of `` in NLog.config. +### 1. `config\config.json` + +> See `stock-config.json` for a complete example. Please input `null` to set a value is empty. + +- **`digicertApiKey`** - CertCentral API key. Use a key created with **API key restrictions = None**. +- **`keyfactorDomainAndUser`** - e.g., `DOMAIN\\Username`. User must be permitted to use the Keyfactor API, create/edit metadata fields, and edit certificates. +- **`keyfactorPassword`** - Password for the Keyfactor user. +- **`keyfactorApiUrl`** - Root Keyfactor API URL, e.g., `https://your-keyfactor-server/keyfactorapi/`. +- **`keyfactorDigicertIssuedCertQueryTerm`** - Substring matched against Issuer DN to identify DigiCert‑issued certificates (e.g., `"DigiCert"`). +- **`importAllCustomDigicertFields`** - If `true`, import all DigiCert custom fields and auto-create Keyfactor metadata fields to match (ignores `CustomFields` entries). +- **`importDataForDeactivatedDigiCertFields`** - If `true`, process DigiCert fields even if deactivated. +- **`syncRevokedAndExpiredCerts`** - If `true`, include revoked and expired certificates in sync. +- **`keyfactorPageSize`** - Batch size for Keyfactor certificate processing (default: `100`). +- **`keyfactorDateFormat`** - Date format for Keyfactor writes (defaults vary by Keyfactor version; `M/d/yyyy h:mm:ss tt` for 25.1, `yyyy-MM-dd` for some older Keyfactor versions). +- **`createMissingFieldsInDigicert`** - If `true` (and `importAllCustomDigicertFields` is `false`), create missing DigiCert custom fields when syncing KF→DC (subject to DigiCert API limitations). + +--- + +### 2. `config\fields.json` + +> See `stock-fields.json` for examples. + +For each mapping: + +- **`digicertFieldName`** - DigiCert field name; for manual fields, a **dot path** into the Order Info JSON. +- **`digicertCustomFieldDataType`** - Input type for DigiCert **custom** fields: + `0` = Anything, `1` = Text, `2` = Int, `3` = EmailAddress, `4` = EmailList. + *(Dropdowns are not supported by the DigiCert API.)* +- **`keyfactorMetadataFieldName`** - Target Keyfactor metadata field name (**[A-Za-z0-9-_]** only; no spaces). +- **`keyfactorDescription`** - Description shown in Keyfactor. +- **`keyfactorDataType`** - Keyfactor type: `1` String, `2` Integer, `3` Date, `4` Boolean, `5` MultipleChoice, `6` BigText, `7` Email. +- **`keyfactorHint`** - UI hint text in Keyfactor. +- **`keyfactorValidation`** - Regex validation (string fields only). +- **`keyfactorEnrollment`** - Enrollment behavior (e.g., `0` Optional, `1` Required, `2` Hidden). +- **`keyfactorMessage`** - Validation failure message. +- **`keyfactorOptions`** - Values for MultipleChoice (ignored otherwise). +- **`keyfactorDefaultValue`** - Default value, if applicable. +- **`keyfactorDisplayOrder`** - Display order in Keyfactor. +- **`keyfactorCaseSensitive`** - Whether validation is case-sensitive (string fields with validation). + +Please review this for the exact values available for each `keyfactor` field: [Keyfactor API Reference](https://software.keyfactor.com/Core-OnPrem/v25.2/Content/WebAPI/KeyfactorAPI/MetadataFieldsPost.htm) + +--- + +### 3. `config\bannedcharacters.json` + +Generated on first `dctokf` run if DigiCert custom field names contain characters not permitted by Keyfactor (only alphanumeric, `-`, and `_` are allowed). Fill in `replacementCharacter` for each banned `character`, then re-run. + +**Example:** + +```jsonc +[ + { "character": " ", "replacementCharacter": "_" }, + { "character": "/", "replacementCharacter": "-" } +] +``` + +If any `replacementCharacter` remains `null`, the tool exits with an error on the next run. + +--- + +### 4. `config\nlog.config` (Logging) + +Logging uses **NLog** and writes to a local `logs/` folder. + +- Configure minimum levels and targets in `rules`. +- Two files are typically produced: a main log (all levels) and an error-only log. + +> Adjust `minLevel` in the `` section to change verbosity. Available levels: `Trace`, `Debug`, `Info`. `Info` for default. + +--- + +## Example Workflow + +1. **Initial Setup** + - Populate `config\config.json` with DigiCert and Keyfactor credentials and settings. + - Define `ManualFields` and `CustomFields` lists in `config\fields.json`. + +2. **First Run (Detect Banned Characters)** + ```powershell + ./DigicertMetadataSync.exe dctokf + ``` + - If banned characters are found in DigiCert custom field names, the tool logs a warning and exits. + - A `bannedcharacters.json` file is created with `replacementCharacter: null` entries. + +3. **Populate Replacements** + - Edit `config\bannedcharacters.json` and set `replacementCharacter` values. + - Save the file. + +4. **Second Run (Create Fields & Sync Data)** + ```powershell + ./DigicertMetadataSync.exe dctokf + ``` + - Fields are created/validated; data is synchronized DigiCert -> Keyfactor. + - (Optional) Run `kftodc` to push Keyfactor values back to DigiCert custom fields. + +--- + +## How It Works + +### DigiCert -> Keyfactor (`dctokf`) +1. Read manual mappings from `fields.json`. +2. Read custom fields from DigiCert (all if `importAllCustomDigicertFields` is `true`; otherwise only those listed). +3. Ensure Keyfactor metadata fields exist (create missing). +4. Query Keyfactor for DigiCert-issued certs (Issuer DN filter). +5. For each certificate: + - Fetch DigiCert order data (manual + custom). + - Coerce types to Keyfactor formats. + - Update Keyfactor metadata values. + +### Keyfactor -> DigiCert (`kftodc`) +1. Read custom field mappings from `fields.json`. +2. Ensure Keyfactor metadata fields exist (create missing). +3. If enabled, create missing DigiCert custom fields (API limitations apply). +4. Query Keyfactor for DigiCert-issued certs. +5. For each certificate: + - Read Keyfactor metadata values. + - Coerce to DigiCert data types. + - Update DigiCert custom field values on the order. + +**Retry logic:** When DigiCert rate-limits, the tool honors the DigiCert-supplied backoff time before retrying. + +--- + +## Usage Recommendations + +- Schedule periodic runs using Windows Task Scheduler (or equivalent). +- Run from the tool's directory; ensure the account can read/write the `config` folder. +- Sync is **destructive** for the destination side (values are overwritten in the destination of the chosen direction). +- Differential change tracking is **not** supported due to DigiCert and Keyfactor API limitations. + +--- + +## Troubleshooting + +- **Authentication errors** - Verify DigiCert API key and Keyfactor credentials/URL. +- **Keyfactor field name errors** - Ensure `bannedcharacters.json` replacements are set and valid. +- **Field creation failures** - Check Keyfactor logs for details; API errors may be non-specific. +- **Custom fields with options in DigiCert** - The DigiCert API cannot create dropdown/option fields; create these manually in CertCentral. + +--- + + + +## License + +Apache License 2.0, see [LICENSE](LICENSE). + +## Related Integrations + +See all [Keyfactor integrations](https://github.com/topics/keyfactor-integration). \ No newline at end of file diff --git a/digicert-metadata-sync/AddFieldsToKeyfactor.cs b/digicert-metadata-sync/AddFieldsToKeyfactor.cs deleted file mode 100644 index 8425582..0000000 --- a/digicert-metadata-sync/AddFieldsToKeyfactor.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2021 Keyfactor -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using Newtonsoft.Json; -using RestSharp; -using RestSharp.Authenticators; - -namespace DigicertMetadataSync; - -// This fuction adds the fields to keyfactor. -// It will only add new fields. -internal partial class DigicertSync -{ - public static Tuple> AddFieldsToKeyfactor(List inputlist, - List existingmetadatalist, bool noexistingfields, string keyfactorusername, - string keyfactorpassword, string keyfactorapilocation) - { - var addfieldstokeyfactorurl = keyfactorapilocation + "MetadataFields"; - var addfieldsclient = new RestClient(); - addfieldsclient.Authenticator = new HttpBasicAuthenticator(keyfactorusername, keyfactorpassword); - var totalnumberadded = 0; - var newfields = new List(); - if (inputlist.Count != 0) - foreach (var metadatainstance in inputlist) - if (noexistingfields == false) - { - var fieldquery = from existingmetadatainstance in existingmetadatalist - where existingmetadatainstance.Name == metadatainstance.Name - select existingmetadatainstance; - // If field does not exist in Keyfactor, add it. - if (!fieldquery.Any()) - { - var addfieldrequest = new RestRequest(addfieldstokeyfactorurl); - addfieldrequest.AddHeader("Content-Type", "application/json"); - addfieldrequest.AddHeader("Accept", "application/json"); - addfieldrequest.AddHeader("x-keyfactor-api-version", "1"); - addfieldrequest.AddHeader("x-keyfactor-requested-with", "APIClient"); - var serializedfield = JsonConvert.SerializeObject(metadatainstance); - addfieldrequest.AddParameter("application/json", serializedfield, ParameterType.RequestBody); - var metadataresponse = new RestResponse(); - try - { - metadataresponse = addfieldsclient.Post(addfieldrequest); - newfields.Add(metadatainstance.Name); - ++totalnumberadded; - } - catch (HttpRequestException e) - { - Console.WriteLine(metadataresponse.Content); - throw e; - } - } - else - { - if (fieldquery.FirstOrDefault().DataType != metadatainstance.DataType) - { - //Throw error if datatype included in keyfactor does not match the digicert one. - var mismatchedtypes = new NotSupportedException(); - throw mismatchedtypes; - } - } - } - else - { - var addfieldrequest = new RestRequest(addfieldstokeyfactorurl); - addfieldrequest.AddHeader("Content-Type", "application/json"); - addfieldrequest.AddHeader("Accept", "application/json"); - addfieldrequest.AddHeader("x-keyfactor-api-version", "1"); - addfieldrequest.AddHeader("x-keyfactor-requested-with", "APIClient"); - var serializedfield = JsonConvert.SerializeObject(metadatainstance); - addfieldrequest.AddParameter("application/json", serializedfield, ParameterType.RequestBody); - var metadataresponse = new RestResponse(); - try - { - metadataresponse = addfieldsclient.Post(addfieldrequest); - ++totalnumberadded; - } - catch (HttpRequestException e) - { - Console.WriteLine(metadataresponse.Content); - throw e; - } - } - - var returnvals = new Tuple>(totalnumberadded, newfields); - - return returnvals; - } -} \ No newline at end of file diff --git a/digicert-metadata-sync/App.config b/digicert-metadata-sync/App.config deleted file mode 100644 index 4cf5c9f..0000000 --- a/digicert-metadata-sync/App.config +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/digicert-metadata-sync/BannedCharacters.cs b/digicert-metadata-sync/BannedCharacters.cs deleted file mode 100644 index 74e33fe..0000000 --- a/digicert-metadata-sync/BannedCharacters.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2021 Keyfactor -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System.Text.RegularExpressions; -using Newtonsoft.Json.Linq; - -namespace DigicertMetadataSync; - -internal partial class DigicertSync -{ - public static List BannedCharacterParse(string input) - { - string pattern = "[a-zA-Z0-9-_]"; - - List bannedChars = new List(); - - foreach (char c in input) - { - if (!Regex.IsMatch(c.ToString(), pattern)) - { - CharDBItem localitem = new CharDBItem(); - localitem.character = c.ToString(); - localitem.replacementcharacter = "null"; - bannedChars.Add(localitem); - } - } - - if (bannedChars.Count > 0) - { - Console.WriteLine("The field name " + input + " contains the following invalid characters: " + - string.Join("", bannedChars.Select(item => item.character))); - } - else - { - Console.WriteLine("The field name " + input + " is valid."); - } - - return bannedChars; - } - - public static void CheckForChars(List input, List allBannedChars, bool restartandconfigrequired) - { - foreach (var dgfield in input) - { - List newChars = BannedCharacterParse(dgfield.DigicertFieldName); - foreach (var newchar in newChars) - { - bool exists = allBannedChars.Any(allcharchar => allcharchar.character == newchar.character); - if (!exists) - { - allBannedChars.Add(newchar); - restartandconfigrequired = true; - } - } - } - } -} diff --git a/digicert-metadata-sync/Clients/DigicertClient.cs b/digicert-metadata-sync/Clients/DigicertClient.cs new file mode 100644 index 0000000..1bb42c7 --- /dev/null +++ b/digicert-metadata-sync/Clients/DigicertClient.cs @@ -0,0 +1,465 @@ +// Copyright 2024 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using DigicertMetadataSync.Client; +using DigicertMetadataSync.Models; +using Microsoft.Extensions.DependencyInjection; +using NLog; + +namespace DigicertMetadataSync.Client +{ + /// + /// Fully synchronous DigiCert CertCentral Services API client built on HttpClient.Send (requires .NET 8+). + /// Matches the style of KeyfactorMetadataClient (sync helpers, streaming JSON). + /// Docs: + /// - Auth header X-DC-DEVKEY + /// - Account custom fields: /services/v2/account/metadata + /// - Orders: /services/v2/order/certificate (list, info) and /custom-field (value edits) + /// + public sealed class DigiCertClient + { + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); + + private static readonly JsonSerializerOptions JsonOpts = new() + { + PropertyNameCaseInsensitive = true, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + private readonly HttpClient _http; + + private readonly JsonSerializerOptions _json = new() + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) } + }; + + public DigiCertClient(HttpClient http) + { + _http = http ?? throw new ArgumentNullException(nameof(http)); + } + + /// + /// Sets required headers. Provide your CertCentral API key. + /// + public void Authenticate(string apiKey) + { + if (string.IsNullOrWhiteSpace(apiKey)) throw new ArgumentNullException(nameof(apiKey)); + var h = _http.DefaultRequestHeaders; + if (h.Contains("X-DC-DEVKEY")) h.Remove("X-DC-DEVKEY"); + h.Add("X-DC-DEVKEY", apiKey); + if (!h.Accept.Any()) h.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + // Many DigiCert examples include Content-Type on GET; unnecessary here. + } + + // ------------------------- Core helpers ----------------------------- + private Uri BuildUri(string relativeOrAbsolute) + { + if (Uri.TryCreate(relativeOrAbsolute, UriKind.Absolute, out var abs)) return abs; + var baseUri = _http.BaseAddress ?? + throw new InvalidOperationException("HttpClient.BaseAddress must be set."); + return new Uri(baseUri, relativeOrAbsolute.TrimStart('/')); + } + + private HttpResponseMessage Send(HttpMethod method, string path, HttpContent? content = null) + { + using var req = new HttpRequestMessage(method, BuildUri(path)); + if (content != null) req.Content = content; + var res = SendWithRetry(req); + res.EnsureSuccessStatusCode(); + return res; + } + + private T? ReadJson(HttpResponseMessage resp) + { + using var s = resp.Content.ReadAsStream(); + return JsonSerializer.Deserialize(s, _json); + } + + private StringContent JsonBody(T body) + { + return new StringContent(JsonSerializer.Serialize(body, _json), Encoding.UTF8, "application/json"); + } + + // --------------------- Account: Custom Fields ----------------------- + + /// + /// GET /services/v2/account/metadata – lists custom order fields. + /// + public List ListCustomFields() + { + var resp = Send(HttpMethod.Get, "account/metadata"); + var root = ReadJson(resp) ?? new DcCustomFieldListRoot(); + return root.Metadata ?? new List(); + } + + /// + /// POST /services/v2/account/metadata – add a single custom field. + /// + public HttpResponseMessage AddCustomField(DcCustomFieldCreate create) + { + if (create is null) throw new ArgumentNullException(nameof(create)); + var resp = Send(HttpMethod.Post, "account/metadata", JsonBody(create)); + return resp; + } + + /// + /// POST /services/v2/account/metadata/bulk – add multiple custom fields. + /// + public HttpResponseMessage BulkAddCustomFields(IEnumerable items) + { + var body = new { metadata = items?.ToList() ?? new List() }; + var resp = Send(HttpMethod.Post, "account/metadata/bulk", JsonBody(body)); + return resp; + } + + /// + /// PUT /services/v2/account/metadata/{metadata_id} + /// + public DcCustomField EditCustomField(int metadataId, DcCustomFieldUpdate update) + { + if (metadataId <= 0) throw new ArgumentOutOfRangeException(nameof(metadataId)); + var resp = Send(HttpMethod.Put, $"account/metadata/{metadataId}", JsonBody(update)); + return ReadJson(resp) ?? new DcCustomField(); + } + + /// + /// DELETE /services/v2/account/metadata/{metadata_id} + /// + public void DeleteCustomField(int metadataId) + { + if (metadataId <= 0) throw new ArgumentOutOfRangeException(nameof(metadataId)); + using var _ = Send(HttpMethod.Delete, $"account/metadata/{metadataId}"); + } + + // --------------------------- Orders -------------------------------- + + /// + /// Updates a single custom field value on a DigiCert order. + /// Throws HttpRequestException on non-success (incl. invalid_custom_field_value). + /// + public bool UpdateOrderCustomFieldValue(int orderId, int metadataId, string value) + { + if (orderId <= 0) throw new ArgumentOutOfRangeException(nameof(orderId)); + if (metadataId <= 0) throw new ArgumentOutOfRangeException(nameof(metadataId)); + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException( + "Value must be non-empty. To remove a value, call DeleteOrderCustomFieldValue.", nameof(value)); + + var url = $"order/certificate/{orderId}/custom-field"; + var payload = new DcCustomFieldValueUpdate { MetadataId = metadataId, Value = value }; + + using var req = new HttpRequestMessage(HttpMethod.Post, url) + { + Content = new StringContent(JsonSerializer.Serialize(payload, JsonOpts), Encoding.UTF8, + "application/json") + }; + + using var res = SendWithRetry(req); // your existing retry (429/5xx) helper + if (res.IsSuccessStatusCode) return true; + if (res.StatusCode != HttpStatusCode.NoContent) + { + var body = ReadString(res); + _logger.Error("DigiCert custom-field update did not return 204. Status={Status}, Body={Body}", + res.StatusCode, body); + res.EnsureSuccessStatusCode(); + } + + return false; + } + + /// + /// GET /services/v2/order/certificate – list orders with optional filters. + /// Supply filters as a dictionary of key to value, e.g. { "serial_number", "..." }. + /// + public DcOrderList ListOrders(IDictionary? filters = null, int? offset = null, + int? limit = null) + { + var sb = new StringBuilder("order/certificate"); + var first = true; + + void AddQ(string k, string v) + { + sb.Append(first ? '?' : '&'); + first = false; + sb.Append(Uri.EscapeDataString(k)).Append('=').Append(Uri.EscapeDataString(v)); + } + + if (filters != null) + foreach (var kv in filters) + AddQ($"filters[{kv.Key}]", kv.Value); + if (offset.HasValue) AddQ("offset", offset.Value.ToString()); + if (limit.HasValue) AddQ("limit", limit.Value.ToString()); + + var resp = Send(HttpMethod.Get, sb.ToString()); + return ReadJson(resp) ?? new DcOrderList(); + } + + /// + /// Returns the full order (DcOrderInfo) including DcOrderCertificate and custom_fields. + /// Will use at most two requests (one if identifier substitution succeeds). + /// + public DcOrderInfo? GetOrderBySerialOrThumbprint(string? serialHex, string? thumbprint) + { + var normSerial = NormalizeHex(serialHex); + var normThumb = NormalizeHex(thumbprint); + + // 1) Fast path: identifier substitution on /order/certificate/{identifier} + if (!string.IsNullOrEmpty(normSerial)) + { + var info = TryGetOrderInfoByIdentifier(normSerial); + if (info != null) return info; + + // Fallback by serial -> order_id -> order + var orderId = FindOrderIdBySerial(normSerial); + if (orderId != null) + return TryGetOrderInfoByIdentifier(orderId.Value.ToString()); + } + + if (!string.IsNullOrEmpty(normThumb)) + { + var info = TryGetOrderInfoByIdentifier(normThumb); + if (info != null) return info; + + // Fallback by thumbprint via Custom Reports -> order_id -> order + var orderId = FindOrderIdByThumbprintViaReports(normThumb); + if (orderId != null) + return TryGetOrderInfoByIdentifier(orderId.Value.ToString()); + } + + _logger?.Warn("Unable to locate DigiCert order for serial {Serial} or thumbprint {Thumb}.", + normSerial ?? "", normThumb ?? ""); + return null; + } + + // ---------------------- Core calls ---------------------- + + private DcOrderInfo? TryGetOrderInfoByIdentifier(string idOrSerialOrThumbprint) + { + using var req = new HttpRequestMessage(HttpMethod.Get, + $"order/certificate/{Uri.EscapeDataString(idOrSerialOrThumbprint)}"); + using var res = SendWithRetry(req); + if (res.StatusCode == HttpStatusCode.NotFound) return null; + res.EnsureSuccessStatusCode(); + + var info = ReadJson(res); + if (info?.Certificate == null && info != null) + _logger?.Debug("Order {OrderId} returned without certificate object.", info.Id); + return info; + } + + private int? FindOrderIdBySerial(string serialHex) + { + var url = $"order/certificate?filters[serial_number]={Uri.EscapeDataString(serialHex)}&limit=1"; + using var req = new HttpRequestMessage(HttpMethod.Get, url); + using var res = SendWithRetry(req); + if (res.StatusCode == HttpStatusCode.NotFound) return null; + res.EnsureSuccessStatusCode(); + + var list = ReadJson(res); + var id = list?.Orders?.FirstOrDefault()?.Id; + _logger?.Debug("FindOrderIdBySerial({Serial}) -> {OrderId}", serialHex, id); + return id; + } + + private int? FindOrderIdByThumbprintViaReports(string thumbprint) + { + var payload = new ReportsQueryRequest + { + Query = "query($t:String!){ order_details(thumbprint:$t, limit:1){ id } }", + Variables = new ReportsQueryVars { T = thumbprint } + }; + + using var req = + new HttpRequestMessage(HttpMethod.Post, new Uri("https://www.digicert.com/services/v2/reports/query")) + { + Content = new StringContent(JsonSerializer.Serialize(payload, JsonOpts), Encoding.UTF8, + "application/json") + }; + + using var res = SendWithRetry(req); + if (res.StatusCode == HttpStatusCode.NotFound) return null; + if (res.StatusCode == HttpStatusCode.BadRequest) return null; + + var doc = ReadJson>(res); + var first = doc?.Data?.OrderDetails?.FirstOrDefault()?.Id; + if (first != null && int.TryParse(first, out var id)) + { + _logger?.Debug("FindOrderIdByThumbprintViaReports({Thumb}) -> {OrderId}", thumbprint, id); + return id; + } + + _logger?.Debug("FindOrderIdByThumbprintViaReports({Thumb}) returned no matches.", thumbprint); + return null; + } + + private static string ReadString(HttpResponseMessage resp) + { + // Choose encoding from Content-Type if present; else detect BOM; else UTF-8 + var charset = resp.Content.Headers.ContentType?.CharSet; + Encoding enc; + try + { + enc = string.IsNullOrWhiteSpace(charset) ? new UTF8Encoding(false) : Encoding.GetEncoding(charset); + } + catch + { + enc = new UTF8Encoding(false); + } + + using var s = resp.Content.ReadAsStream(); // sync + using var sr = new StreamReader(s, enc, true, 8192, false); + return sr.ReadToEnd(); + } + + private static byte[] ReadAllBytes(HttpContent content) + { + using var s = content.ReadAsStream(); // sync + using var ms = new MemoryStream(); + s.CopyTo(ms); // sync + return ms.ToArray(); + } + + private HttpResponseMessage SendWithRetry(HttpRequestMessage req) + { + const int maxAttempts = 4; + var rnd = new Random(); + + for (var attempt = 1; attempt <= maxAttempts; attempt++) + { + HttpResponseMessage res; + try + { + res = _http.Send(req.Clone()); + } + catch (Exception ex) when (attempt < maxAttempts) + { + var delay1 = BackoffDelay(attempt, rnd); + _logger?.Warn(ex, "Request send failed (attempt {Attempt}/{Max}). Backing off {Delay}ms.", attempt, + maxAttempts, (int)delay1.TotalMilliseconds); + Thread.Sleep(delay1); + continue; + } + + if (IsFinal(res.StatusCode)) + return res; + + // 429/5xx -> back off, maybe honor Retry-After + if (attempt == maxAttempts) return res; + + var delay2 = GetRetryAfter(res) ?? BackoffDelay(attempt, rnd); + _logger?.Warn("Received {Status} (attempt {Attempt}/{Max}). Backing off {Delay}ms.", + (int)res.StatusCode, attempt, maxAttempts, (int)delay2.TotalMilliseconds); + Thread.Sleep(delay2); + } + + throw new InvalidOperationException("Unreachable retry loop termination."); + } + + private static bool IsFinal(HttpStatusCode code) + { + if (code == (HttpStatusCode)429) return false; // Too Many Requests + var n = (int)code; + if (n >= 500 && n <= 599) return false; // 5xx transient + return true; // all others are final + } + + private static TimeSpan? GetRetryAfter(HttpResponseMessage res) + { + if (res.Headers.TryGetValues("Retry-After", out var vals)) + { + var s = vals.FirstOrDefault(); + if (int.TryParse(s, out var seconds)) + return TimeSpan.FromSeconds(Math.Clamp(seconds, 1, 30)); + } + + return null; + } + + private static TimeSpan BackoffDelay(int attempt, Random rnd) + { + // 250, 500, 1000 ms … with +/- up to 200 ms jitter + var baseMs = (int)Math.Pow(2, attempt) * 250; + return TimeSpan.FromMilliseconds(baseMs + rnd.Next(0, 200)); + } + + private static string? NormalizeHex(string? input) + { + if (string.IsNullOrWhiteSpace(input)) return null; + // keep only hex, make uppercase (handles serials and thumbprints) + var filtered = new string(input.Where(Uri.IsHexDigit).ToArray()); + return filtered.Length == 0 ? null : filtered.ToUpperInvariant(); + } + } +} + +// HttpRequestMessage is single-use; clone content/headers for retries +// HttpRequestMessage is single-use; clone so we can retry safely +internal static class HttpRequestMessageExtensions +{ + public static HttpRequestMessage Clone(this HttpRequestMessage req) + { + var clone = new HttpRequestMessage(req.Method, req.RequestUri); + + // headers + foreach (var h in req.Headers) + clone.Headers.TryAddWithoutValidation(h.Key, h.Value); + + // content (buffer if present) + if (req.Content != null) + { + var bytes = req.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult(); + var content = new ByteArrayContent(bytes); + foreach (var h in req.Content.Headers) + content.Headers.TryAddWithoutValidation(h.Key, h.Value); + clone.Content = content; + } + + return clone; + } +} + +#region Models + +public sealed class RateLimitException : Exception +{ + public RateLimitException(string message, int? retryAfterSeconds = null, Exception? inner = null) + : base(message, inner) + { + RetryAfterSeconds = retryAfterSeconds; + } + + public int? RetryAfterSeconds { get; } +} + +#endregion + + +public static class DigiCertServiceCollectionExtensions +{ + /// + /// Registers a DigiCertClient configured for CertCentral Services API. + /// BaseAddress should be set to "https://www.digicert.com/services/v2/". + /// + public static IServiceCollection AddDigiCertClient(this IServiceCollection services, string baseAddress) + { + services.AddHttpClient(client => + { + client.BaseAddress = new Uri(baseAddress.TrimEnd('/') + "/"); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + // DigiCert requires X-DC-DEVKEY; call Authenticate(apiKey) after DI construction. + }); + return services; + } +} \ No newline at end of file diff --git a/digicert-metadata-sync/Clients/KeyfactorClient.cs b/digicert-metadata-sync/Clients/KeyfactorClient.cs new file mode 100644 index 0000000..8abbfa5 --- /dev/null +++ b/digicert-metadata-sync/Clients/KeyfactorClient.cs @@ -0,0 +1,459 @@ +// Copyright 2024 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. +using System.Globalization; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using DigicertMetadataSync.Client; +using DigicertMetadataSync.Models; +using Microsoft.Extensions.DependencyInjection; +using NLog; + +namespace DigicertMetadataSync.Client +{ + /// + /// Fully synchronous Keyfactor client implemented around HttpClient.Send (NET 8+). + /// + public class KeyfactorMetadataClient + { + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); + private readonly HttpClient _httpClient; + + private readonly JsonSerializerOptions _jsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + public KeyfactorMetadataClient(HttpClient httpClient) + { + _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + } + + /// + /// Configure basic auth and required headers. + /// + public void Authenticate(string username, string password, string requestedWith = "APIClient") + { + if (username is null) throw new ArgumentNullException(nameof(username)); + if (password is null) throw new ArgumentNullException(nameof(password)); + + var creds = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}")); + var headers = _httpClient.DefaultRequestHeaders; + headers.Authorization = new AuthenticationHeaderValue("Basic", creds); + headers.Remove("x-keyfactor-requested-with"); + headers.Add("x-keyfactor-requested-with", requestedWith); + } + + // ---- Helpers ------------------------------------------------------ + private Uri BuildUri(string relativePath, string? query = null) + { + var baseUri = _httpClient.BaseAddress ?? + throw new InvalidOperationException("HttpClient.BaseAddress must be set."); + var path = relativePath.TrimStart('/') + (string.IsNullOrEmpty(query) + ? string.Empty + : (relativePath.Contains('?') ? "&" : "?") + query); + return new Uri(baseUri, path); + } + + private HttpResponseMessage Send(HttpMethod method, string relativePath, HttpContent? content = null) + { + using var req = new HttpRequestMessage(method, BuildUri(relativePath)); + if (content != null) + req.Content = content; + + var resp = _httpClient.Send(req); // synchronous, NET 8+ + resp.EnsureSuccessStatusCode(); + return resp; + } + + private T? ReadJson(HttpResponseMessage resp) + { + using var s = resp.Content.ReadAsStream(); // synchronous + return JsonSerializer.Deserialize(s, _jsonOptions); + } + + private string ReadString(HttpResponseMessage resp) + { + using var s = resp.Content.ReadAsStream(); // synchronous + using var sr = new StreamReader(s, Encoding.UTF8, true, 8192, false); + return sr.ReadToEnd(); + } + + private StringContent JsonBody(T value) + { + return new StringContent(JsonSerializer.Serialize(value, _jsonOptions), Encoding.UTF8, "application/json"); + } + + // ---- API methods -------------------------------------------------- + + /// + /// Lists metadata field definitions. + /// + public List ListMetadataFields() + { + var resp = Send(HttpMethod.Get, "MetadataFields"); + return ReadJson>(resp) ?? new List(); + } + + /// + /// Sends/Upserts unified metadata fields to Keyfactor. + /// + public void SendUnifiedMetadataFields(List unifiedFields, + List existingFields) + { + if (unifiedFields is null || unifiedFields.Count == 0) + throw new ArgumentException("The list of unified metadata fields cannot be null or empty.", + nameof(unifiedFields)); + + existingFields ??= new List(); + + var created = 0; + var updated = 0; + + foreach (var field in unifiedFields) + try + { + var existing = existingFields.FirstOrDefault(f => + f.Name.Equals(field.KeyfactorMetadataFieldName, StringComparison.OrdinalIgnoreCase)); + KeyfactorMetadataField payload; + HttpResponseMessage resp; + if (existing is not null) + { + // PUT: include Id + payload = new KeyfactorMetadataField + { + Id = existing.Id, + Name = field.KeyfactorMetadataFieldName, + Description = field.KeyfactorDescription, + DataType = (int)field.KeyfactorDataType, + Hint = field.KeyfactorHint, + Validation = field.KeyfactorValidation, + Enrollment = field.KeyfactorEnrollment, + Message = field.KeyfactorMessage, + Options = field.KeyfactorOptions != null ? string.Join(",", field.KeyfactorOptions) : null, + DefaultValue = field.KeyfactorDefaultValue, + DisplayOrder = field.KeyfactorDisplayOrder, + CaseSensitive = field.KeyfactorCaseSensitive + }; + var json = JsonSerializer.Serialize(payload, _jsonOptions); + _logger.Trace($"Sending JSON Payload: {json}"); + resp = Send(HttpMethod.Put, "MetadataFields", JsonBody(payload)); + updated++; + } + else + { + // POST: do not include Id + payload = new KeyfactorMetadataField + { + Id = 0, + Name = field.KeyfactorMetadataFieldName, + Description = field.KeyfactorDescription, + DataType = (int)field.KeyfactorDataType, + Hint = field.KeyfactorHint, + Validation = field.KeyfactorValidation, + Enrollment = field.KeyfactorEnrollment, + Message = field.KeyfactorMessage, + Options = field.KeyfactorOptions != null ? string.Join(",", field.KeyfactorOptions) : null, + DefaultValue = field.KeyfactorDefaultValue, + DisplayOrder = field.KeyfactorDisplayOrder, + CaseSensitive = field.KeyfactorCaseSensitive + }; + var json = JsonSerializer.Serialize(payload, _jsonOptions); + _logger.Trace($"Sending JSON Payload: {json}"); + resp = Send(HttpMethod.Post, "MetadataFields", JsonBody(payload)); + created++; + } + + + var returned = ReadJson(resp); + if (returned is not null) + { + field.KeyfactorMetadataFieldId = returned.Id; + _logger.Trace( + $"Field '{field.KeyfactorMetadataFieldName}' updated with KeyfactorMetadataFieldId: {field.KeyfactorMetadataFieldId}"); + } + } + catch (Exception ex) + { + _logger.Error(ex, $"Error processing metadata field: {field.KeyfactorMetadataFieldName}"); + } + + _logger.Info($"Metadata fields processed: {created} created, {updated} updated."); + } + + /// + /// Get certificates by issuer (simple, all pages default). For paging, use the overload. + /// + public List GetCertificatesByIssuer(string issuerSubstring = "Sectigo", + bool includeRevokedAndExpired = false) + { + return GetCertificatesByIssuer(issuerSubstring, includeRevokedAndExpired, 1, 100); + } + + /// + /// Get certificates by issuer (paged). + /// + public List GetCertificatesByIssuer(string issuerSubstring, bool includeRevokedAndExpired, + int pageNumber, int pageSize) + { + if (string.IsNullOrWhiteSpace(issuerSubstring)) + throw new ArgumentException("Issuer substring cannot be null or empty.", nameof(issuerSubstring)); + + if (pageNumber <= 0) pageNumber = 1; + if (pageSize <= 0) pageSize = 100; + + var q = $"IssuerDN -contains \"{issuerSubstring}\""; + var encoded = Uri.EscapeDataString(q); + + var query = new StringBuilder( + $"QueryString={encoded}" + + $"&includeMetadata=true" + + $"&PageReturned={pageNumber}" + + $"&ReturnLimit={pageSize}" + + $"&SortField=NotBefore" + // issued-on + $"&SortAscending=1" // 1 = descending (most recent first) + ); + + if (includeRevokedAndExpired) query.Append("&IncludeRevoked=true&IncludeExpired=true"); + + var resp = Send(HttpMethod.Get, $"Certificates?{query}"); + + var json = ReadString(resp); + _logger.Trace($"Raw JSON Response from GetCertificatesByIssuer: {json}"); + + try + { + return JsonSerializer.Deserialize>(json, _jsonOptions) ?? + new List(); + } + catch (JsonException ex) + { + _logger.Error(ex, "Failed to deserialize the certificate list from Keyfactor API."); + throw new InvalidOperationException("Failed to deserialize the certificate list from Keyfactor API.", + ex); + } + } + + /// + /// Update certificate metadata (typed payload). Prefer this overload. + /// Sends integers and booleans as JSON numbers/bools; other types as strings. + /// + public bool UpdateCertificateMetadata(int certificateId, IReadOnlyDictionary metadata) + { + if (certificateId <= 0) + throw new ArgumentException("Certificate ID must be greater than zero.", nameof(certificateId)); + if (metadata is null || metadata.Count == 0) + throw new ArgumentException("Metadata cannot be null or empty.", nameof(metadata)); + + // Final pass: sanitize strings, normalize numeric/bool types, drop null/blank values. + var normalized = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in metadata) + { + if (string.IsNullOrWhiteSpace(kvp.Key)) + continue; + + var v = NormalizeForWire(kvp.Value, out var keep); + if (keep) + normalized[kvp.Key] = v; + } + + if (normalized.Count == 0) + throw new ArgumentException("No non-empty metadata values after normalization.", nameof(metadata)); + + var body = new { Id = certificateId, Metadata = normalized }; + + // Log the exact JSON we're about to send (useful for troubleshooting type issues). + var json = JsonSerializer.Serialize(body, _jsonOptions); + _logger.Trace($"Sending JSON Payload to update metadata: {json}"); + + try + { + var resp = Send(HttpMethod.Put, "Certificates/Metadata", JsonBody(body)); + using (resp) + { + /* disposing response */ + } + + return true; + } + catch (Exception ex) + { + _logger.Error(ex, $"Failed to update metadata for certificate ID: {certificateId}"); + return false; + } + + // ---- local helpers ---- + + static object? NormalizeForWire(object? value, out bool keep) + { + keep = true; + if (value is null) + { + keep = false; + return null; + } + + switch (value) + { + // Already-typed primitives pass through + case bool b: + return b; + + case sbyte or byte or short or ushort or int or uint or long: + // cast all signed/unsigned integrals to long for uniform JSON emission + return Convert.ToInt64(value, CultureInfo.InvariantCulture); + + case ulong ul: + // STJ writes ulong; Keyfactor integer fields are signed. Clamp if needed. + if (ul > long.MaxValue) ul = long.MaxValue; + return (long)ul; + + case float f: + // If it’s integral, keep as integer; else round toward zero + return (long)f; + + case double d: + return (long)d; + + case decimal m: + return (long)m; + + case DateTime dt: + // Use ISO-8601 UTC string + return dt.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture); + + case DateTimeOffset dto: + return dto.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture); + + case string s: + { + var cleaned = SanitizeString(s); + if (string.IsNullOrWhiteSpace(cleaned)) + { + keep = false; + return null; + } + + return cleaned; + } + + // If a JsonElement slipped through, preserve its JSON scalar type where possible. + case JsonElement el: + { + switch (el.ValueKind) + { + case JsonValueKind.Null: + case JsonValueKind.Undefined: + keep = false; + return null; + case JsonValueKind.True: return true; + case JsonValueKind.False: return false; + case JsonValueKind.Number: + if (el.TryGetInt64(out var n)) return n; + if (el.TryGetDouble(out var dbl)) return (long)dbl; + return SanitizeString(el.GetRawText()); + case JsonValueKind.String: + var s = el.GetString(); + var cleaned = SanitizeString(s); + if (string.IsNullOrWhiteSpace(cleaned)) + { + keep = false; + return null; + } + + return cleaned; + default: + // objects/arrays should have been flattened earlier; keep raw JSON as string + var raw = el.GetRawText(); + var cleanedJson = SanitizeString(raw, false); + if (string.IsNullOrWhiteSpace(cleanedJson)) + { + keep = false; + return null; + } + + return cleanedJson; + } + } + + default: + // Fallback: ToString() + sanitize (avoids exceptions on unexpected types) + var txt = SanitizeString(value.ToString()); + if (string.IsNullOrWhiteSpace(txt)) + { + keep = false; + return null; + } + + return txt; + } + } + + static string SanitizeString(string? s, bool collapseInnerWhitespace = true) + { + if (string.IsNullOrEmpty(s)) return string.Empty; + + // Normalize Unicode, drop hidden/zero-width/bidi, replace NBSP, remove control chars (except CR/LF/TAB) + Span hidden = stackalloc char[] + { + '\u200B', '\u200C', '\u200D', '\uFEFF', '\u200E', '\u200F', '\u202A', '\u202B', '\u202C', '\u202D', + '\u202E' + }; + var norm = s.Normalize(NormalizationForm.FormKC); + var sb = new StringBuilder(norm.Length); + foreach (var ch in norm) + { + var isHidden = false; + for (var i = 0; i < hidden.Length; i++) + if (ch == hidden[i]) + { + isHidden = true; + break; + } + + if (isHidden) continue; + + if (ch == '\u00A0') + { + sb.Append(' '); + continue; + } // NBSP -> space + + if (char.IsControl(ch) && ch != '\r' && ch != '\n' && ch != '\t') continue; + sb.Append(ch); + } + + var cleaned = sb.ToString().Trim(); + + if (!collapseInnerWhitespace) return cleaned; + + // Collapse all whitespace to single spaces + return Regex.Replace(cleaned, @"\s+", " ").Trim(); + } + } + } +} + +/// +/// DI registration helpers. +/// +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddKeyfactorMetadataClient(this IServiceCollection services, string baseAddress) + { + services.AddHttpClient(client => + { + client.BaseAddress = new Uri(baseAddress); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + }); + return services; + } +} \ No newline at end of file diff --git a/digicert-metadata-sync/DigicertMetadataSync.csproj b/digicert-metadata-sync/DigicertMetadataSync.csproj index 11cf61a..f76162a 100644 --- a/digicert-metadata-sync/DigicertMetadataSync.csproj +++ b/digicert-metadata-sync/DigicertMetadataSync.csproj @@ -1,33 +1,43 @@  - - Exe - net6.0 - DigicertMetadataSync - enable - enable - + + Exe + net9.0 + DigicertMetadataSync + enable + enable + - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - Always - - + + + + + + + + PreserveNewest + + + Always + + + Always + + + Never + + + Always + + + Always + + + Always + + + Always + + - + \ No newline at end of file diff --git a/digicert-metadata-sync/GrabCustomFieldsFromDigiCert.cs b/digicert-metadata-sync/GrabCustomFieldsFromDigiCert.cs deleted file mode 100644 index 8795602..0000000 --- a/digicert-metadata-sync/GrabCustomFieldsFromDigiCert.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2021 Keyfactor -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using Keyfactor.Logging; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using NLog.Time; -using RestSharp; -using RestSharp.Authenticators; - -namespace DigicertMetadataSync; - -// This fuction adds the fields to keyfactor. -// It will only add new fields. -partial class DigicertSync -{ - public static List GrabCustomFieldsFromDigiCert(string apikey, bool importdeactivated) - { - ILogger logger = LogHandler.GetClassLogger(); - var digicertclient = new RestClient(); - var customfieldsretrieval = "https://www.digicert.com/services/v2/account/metadata"; - var digicertrequest = new RestRequest(customfieldsretrieval); - digicertrequest.AddHeader("Accept", "application/json"); - digicertrequest.AddHeader("X-DC-DEVKEY", apikey); - var digicertresponse = digicertclient.Execute(digicertrequest); - var trimmeddigicertresponse = digicertresponse.Content.Remove(0, 12); - int lengthofresponse = trimmeddigicertresponse.Length; - trimmeddigicertresponse = trimmeddigicertresponse.Remove(lengthofresponse - 1, 1); - var fieldlist = JsonConvert.DeserializeObject>(trimmeddigicertresponse); - if (importdeactivated == false) - { - fieldlist.RemoveAll(unit => unit.is_active == false); - } - Console.WriteLine("Obtained custom fields from DigiCert."); - logger.LogDebug("Obtained custom fields from DigiCert."); - return fieldlist; - } -} \ No newline at end of file diff --git a/digicert-metadata-sync/Helpers.cs b/digicert-metadata-sync/Helpers.cs deleted file mode 100644 index 0246749..0000000 --- a/digicert-metadata-sync/Helpers.cs +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2021 Keyfactor -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -using System.Collections.Generic; - -using System.Text.RegularExpressions; -using Newtonsoft.Json.Linq; - -namespace DigicertMetadataSync; - -internal partial class DigicertSync -{ - public static int TypeMatcher(string digicerttype) - { - if (digicerttype.Contains("int") || digicerttype.Contains("Int")) - // 2 matches the keyfactor int type metadata field - return 2; - //1 matches the keyfactor string type - return 1; - } -} - -internal partial class DigicertSync -{ - public static Dictionary ClassConverter(object obj) - { - if (obj != null && obj != "") - { - var resultdict = new Dictionary(); - var propertylist = obj.GetType().GetProperties(); - - foreach (var prop in propertylist) - { - var propName = prop.Name; - var val = obj.GetType().GetProperty(propName).GetValue(obj, null); - if (val != null) - resultdict.Add(propName, val); - else - resultdict.Add(propName, ""); - } - - return resultdict; - } - - return null; - } - - public static string ReplaceAllBannedCharacters(string input, ListallBannedChars) - { - foreach (CharDBItem item in allBannedChars) - { - input = input.Replace(item.character, item.replacementcharacter); - } - return input; - } - - public static bool CheckMode(string mode) - { - if (mode == "kftodc" || mode == "dctokf") return true; - return false; - } - - private static List convertlisttokf(List inputlist, List allBannedChars, bool importallcustomfields) - { - var formattedlist = new List(); - if (inputlist.Count != 0) - foreach (var input in inputlist) - { - var formatinstance = new KeyfactorMetadataInstanceSendoff(); - - if (input.KeyfactorMetadataFieldName == null || input.KeyfactorMetadataFieldName == "" || input.FieldType == "Custom") - //If name is empty, clean up the characters. - formatinstance.Name = ReplaceAllBannedCharacters(input.DigicertFieldName, allBannedChars); - - else - //Use user input preferred name. - formatinstance.Name = input.KeyfactorMetadataFieldName; - - formatinstance.AllowAPI = Convert.ToBoolean(input.KeyfactorAllowAPI); - formatinstance.Hint = input.KeyfactorHint; - formatinstance.DataType = TypeMatcher(input.KeyfactorDataType); - formatinstance.Description = input.KeyfactorDescription; - formattedlist.Add(formatinstance); - } - - - - return formattedlist; - } - - public static JObject Flatten(JObject jObject, string parentName = "") - { - var result = new JObject(); - foreach (var property in jObject.Properties()) - { - var propName = string.IsNullOrEmpty(parentName) ? property.Name : $"{parentName}.{property.Name}"; - if (property.Value is JObject nestedObject) - result.Merge(Flatten(nestedObject, propName)); - else - result[propName] = property.Value; - } - - return result; - } -} \ No newline at end of file diff --git a/digicert-metadata-sync/Logic/BannedCharacters.cs b/digicert-metadata-sync/Logic/BannedCharacters.cs new file mode 100644 index 0000000..e5e5e8a --- /dev/null +++ b/digicert-metadata-sync/Logic/BannedCharacters.cs @@ -0,0 +1,126 @@ +// Copyright 2024 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. +using System.Text.RegularExpressions; +using DigicertMetadataSync.Models; +using NLog; + +namespace DigicertMetadataSync.Logic; + +/// +/// Provides utilities for identifying and replacing banned characters in field names. +/// +public class BannedCharacters +{ + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); + + /// + /// Parses a given string to identify characters that are not allowed (banned characters). + /// + /// The string to parse for banned characters. + /// A list to store details about invalid characters found in the input. + /// A list of objects representing the banned characters and their replacements. + public static List BannedCharacterParse(string input, List invalidCharacterDetails) + { + var pattern = "[a-zA-Z0-9-_]"; + var bannedChars = new List(); + var uniqueCharacters = new HashSet(); // Track unique banned characters + + foreach (var c in input) + if (!Regex.IsMatch(c.ToString(), pattern) && uniqueCharacters.Add(c.ToString())) + { + var localitem = new CharDBItem + { + character = c.ToString(), + replacementcharacter = "null" + }; + bannedChars.Add(localitem); + + // Add details for aggregation + invalidCharacterDetails.Add( + $"The field name '{input}' contains the invalid character: '{c}' (U+{(int)c:X4})"); + } + + return bannedChars; + } + + /// + /// Checks a list of metadata fields for banned characters in their names. + /// + /// The list of objects to check. + /// A list to store all banned characters found across the fields. + /// A list to store details about invalid characters found in the fields. + /// + /// If true, checks the for banned characters. + /// Otherwise, checks the or + /// + /// depending on the field type. + /// + public static void CheckForChars(List input, List allBannedChars, + List invalidCharacterDetails, bool noAuto = false) + { + foreach (var dcField in input) + { + // Option 1 - Custom Field, Auto conversion - in this case we need to check the sectigo field name, as it is used for conversion + var fieldName = dcField.DigicertFieldName; + // Option 2 - Manual Field 9 (auto and !auto) - in this case we only check the Keyfactor Field Name + if (dcField.ToolFieldType == UnifiedFieldType.Manual) fieldName = dcField.KeyfactorMetadataFieldName; + // Option 3, Custom Field, !Auto and Manual Loading - in this case we check the loaded Keyfactor Field Name, as no conversion is done + if (noAuto) fieldName = dcField.KeyfactorMetadataFieldName; + var newChars = BannedCharacterParse(fieldName, invalidCharacterDetails); + foreach (var newchar in newChars) + { + var exists = allBannedChars.Any(allcharchar => allcharchar.character == newchar.character); + if (!exists) allBannedChars.Add(newchar); + } + } + + _logger.Info( + $"Checked {input.Count} fields for banned characters."); + if (invalidCharacterDetails.Count > 0) + _logger.Warn( + $"The following invalid characters were found in the field names: {string.Join(", ", invalidCharacterDetails)}"); + } + + /// + /// Replaces all banned characters in a string with their corresponding replacement characters. + /// + /// The string to sanitize by replacing banned characters. + /// + /// A list of objects representing banned characters and their + /// replacements. + /// + /// The sanitized string with banned characters replaced. + public static string ReplaceAllBannedCharacters(string input, List allBannedChars) + { + var missingReplacements = new List(); + var finalString = input; + var conversionOccurred = false; // Track if any conversion has taken place + + foreach (var item in allBannedChars) + if (item.replacementcharacter == "null") + { + missingReplacements.Add($"'{item.character}' (U+{(int)item.character[0]:X4})"); + } + else + { + if (finalString.Contains(item.character)) + { + finalString = finalString.Replace(item.character, item.replacementcharacter); + conversionOccurred = true; // Mark that a conversion has occurred + } + } + + if (missingReplacements.Count > 0) + _logger.Warn( + $"The field name '{input}' has banned characters with no replacements: {string.Join(", ", missingReplacements)}"); + else if (conversionOccurred) + _logger.Info( + $"The Sectigo field name '{input}' will be converted to: '{finalString}' for use with Keyfactor."); + + return finalString; + } +} \ No newline at end of file diff --git a/digicert-metadata-sync/Logic/Helpers.cs b/digicert-metadata-sync/Logic/Helpers.cs new file mode 100644 index 0000000..1c0db20 --- /dev/null +++ b/digicert-metadata-sync/Logic/Helpers.cs @@ -0,0 +1,269 @@ +// Copyright 2024 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. +using System.Collections; +using System.Globalization; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using DigicertMetadataSync.Models; + +namespace DigicertMetadataSync.Logic; + +public class Helpers +{ + // Map DigiCert's *string* style (e.g., "text", "int") to Keyfactor enum + public static KeyfactorMetadataDataType ToKeyfactorDataType(string? dcDataType) + { + var t = (dcDataType ?? "text").Trim().ToLowerInvariant(); + return t switch + { + "text" or "string" => KeyfactorMetadataDataType.String, + "int" or "integer" or "number" => KeyfactorMetadataDataType.Integer, + "date" or "datetime" => KeyfactorMetadataDataType.Date, + "bool" or "boolean" => KeyfactorMetadataDataType.Boolean, + // Setting to string due to no way to retrieve options existing. + "select" or "drop_down_menu" or "picklist" + or "options" or "choice" => KeyfactorMetadataDataType.String, + "textarea" or "multiline" => KeyfactorMetadataDataType.BigText, + "email" or "email_address" or "email_list" => KeyfactorMetadataDataType.Email, + _ => KeyfactorMetadataDataType.String + }; + } + + // Map DigiCert's *enum* to Keyfactor enum (used when you lift from account metadata or fields.json) + public static KeyfactorMetadataDataType ToKeyfactorDataType(DigiCertCustomFieldDataType dc) + { + return dc switch + { + DigiCertCustomFieldDataType.Anything => KeyfactorMetadataDataType.String, + DigiCertCustomFieldDataType.Text => KeyfactorMetadataDataType.String, + DigiCertCustomFieldDataType.Int => KeyfactorMetadataDataType.Integer, + DigiCertCustomFieldDataType.EmailAddress => KeyfactorMetadataDataType.Email, + DigiCertCustomFieldDataType.EmailList => KeyfactorMetadataDataType + .Email, // Keyfactor has single Email data type + _ => KeyfactorMetadataDataType.String + }; + } + /// + /// Maps DigiCert wire "data_type" string to DigiCertCustomFieldDataType enum. + /// Accepts common synonyms and numeric strings; null/empty => Anything. + /// + public static DigiCertCustomFieldDataType ToDigiCertEnumFromString(string? dataType) + { + if (string.IsNullOrWhiteSpace(dataType)) return DigiCertCustomFieldDataType.Anything; + + var key = dataType.Trim() + .Replace("-", "_") + .Replace(" ", "_") + .ToLowerInvariant(); + + // Numeric string? (rare, but makes config/backfills forgiving) + if (int.TryParse(key, out var code) && + Enum.IsDefined(typeof(DigiCertCustomFieldDataType), code)) + { + return (DigiCertCustomFieldDataType)code; + } + + return key switch + { + "text" or "string" => DigiCertCustomFieldDataType.Text, + "int" or "integer" or "number" => DigiCertCustomFieldDataType.Int, + "email_address" or "email" => DigiCertCustomFieldDataType.EmailAddress, + "email_list" or "emails" or "email_address_list" + => DigiCertCustomFieldDataType.EmailList, + _ => DigiCertCustomFieldDataType.Anything + }; + } + + + // Map Keyfactor → DigiCert (used only as fallback) + public static DigiCertCustomFieldDataType ToDigiCertDataType(KeyfactorMetadataDataType kf) + { + return kf switch + { + KeyfactorMetadataDataType.Integer => DigiCertCustomFieldDataType.Int, + KeyfactorMetadataDataType.Email => DigiCertCustomFieldDataType.EmailAddress, + // Keyfactor has no "email list" concept; keep it Text unless you specify via fields.json + KeyfactorMetadataDataType.String => DigiCertCustomFieldDataType.Text, + KeyfactorMetadataDataType.BigText => DigiCertCustomFieldDataType.Text, + KeyfactorMetadataDataType.MultipleChoice => DigiCertCustomFieldDataType.Text, + KeyfactorMetadataDataType.Date => DigiCertCustomFieldDataType.Text, + KeyfactorMetadataDataType.Boolean => DigiCertCustomFieldDataType.Text, + _ => DigiCertCustomFieldDataType.Text + }; + } + + /// + /// Maps Keyfactor's Enrollment (0=Optional, 1=Required, 2=Hidden/Not used) + /// into DigiCert's custom-field booleans. + /// + public static (bool IsRequired, bool IsActive) ToDigiCertFlags(int keyfactorEnrollment) + { + return keyfactorEnrollment switch + { + 1 => (true, true), // Required + 2 => (false, false), // Hidden -> make field inactive in DigiCert + _ => (false, true) // Optional (default) + }; + } + + + // Mirrors: public static JObject Flatten(JObject jObject, string parentName = "") + public static JsonObject Flatten(JsonObject obj, string parentName = "") + { + if (obj is null) throw new ArgumentNullException(nameof(obj)); + + var result = new JsonObject(); + + void Recurse(JsonNode? node, string prefix) + { + switch (node) + { + case JsonObject o: + foreach (var kvp in o) + { + var name = string.IsNullOrEmpty(prefix) ? kvp.Key : $"{prefix}.{kvp.Key}"; + Recurse(kvp.Value, name); + } + + break; + + case JsonArray arr: + for (var i = 0; i < arr.Count; i++) + { + var name = string.IsNullOrEmpty(prefix) ? $"[{i}]" : $"{prefix}[{i}]"; + Recurse(arr[i], name); + } + + break; + + default: + // Leaf (string/number/bool/null). DeepClone to detach from source graph. + result[prefix] = node?.DeepClone(); + break; + } + } + + Recurse(obj, parentName ?? string.Empty); + return result; + } + + /// + /// Resolves a dot-path on an object graph using reflection and [JsonPropertyName] matches. + /// If stringifyLeaf=true, collections/JsonElement/etc. are converted to a scalar string. + /// + public static object? GetPropertyValue(object root, string path, bool stringifyLeaf = true) + { + if (root is null) throw new ArgumentNullException(nameof(root)); + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Path cannot be null or empty.", nameof(path)); + + var current = root; + + foreach (var propertyName in path.Split('.', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + if (current is null) return null; + + // Support JsonElement containers too + if (current is JsonElement je && je.ValueKind == JsonValueKind.Object) + { + if (!je.TryGetProperty(propertyName, out je)) return null; + current = je; + continue; + } + + var props = current.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); + var pi = props.FirstOrDefault(p => + string.Equals(p.Name, propertyName, StringComparison.OrdinalIgnoreCase) || + p.GetCustomAttribute()?.Name == propertyName); + + if (pi is null) return null; + current = pi.GetValue(current); + } + + return stringifyLeaf ? CoerceToScalarString(current) : current; + } + + /// + /// Convenience: always get a printable scalar string. + /// + public static string? GetPropertyValueAsString(object root, string path) + { + return CoerceToScalarString(GetPropertyValue(root, path, false)); + } + + private static string? CoerceToScalarString(object? value) + { + if (value is null) return null; + + // Already string + if (value is string s) return s; + + // JsonElement -> scalar/CSV/JSON + if (value is JsonElement je) return JsonElementToString(je); + + // Date/Time + if (value is DateTime dt) return dt.ToString("O", CultureInfo.InvariantCulture); + if (value is DateTimeOffset dto) return dto.ToString("O", CultureInfo.InvariantCulture); + + // Numbers/booleans/etc. + if (value is IFormattable f) return f.ToString(null, CultureInfo.InvariantCulture); + + // IEnumerable -> CSV + if (value is IEnumerable enumerable && value is not IEnumerable) + { + var parts = new List(); + foreach (var item in enumerable) + { + var text = CoerceToScalarString(item); + if (!string.IsNullOrWhiteSpace(text)) + parts.Add(text); + } + + return parts.Count == 0 ? null : string.Join(", ", parts); + } + + // Complex object -> stable JSON snapshot + return JsonSerializer.Serialize(value); + } + + private static string? JsonElementToString(JsonElement el) + { + return el.ValueKind switch + { + JsonValueKind.String => el.GetString(), + JsonValueKind.Number => el.GetRawText(), + JsonValueKind.True => "true", + JsonValueKind.False => "false", + JsonValueKind.Null => null, + JsonValueKind.Array => string.Join(", ", + el.EnumerateArray() + .Select(JsonElementToString) + .Where(x => !string.IsNullOrWhiteSpace(x))), + JsonValueKind.Object => el.GetRawText(), + _ => el.GetRawText() + }; + } + + public static class DcCustomFieldTypeMapper + { + // enum -> wire string + public static string? ToWireString(DigiCertCustomFieldDataType t) + { + return t switch + { + DigiCertCustomFieldDataType.Anything => null, // omit to mean "anything" + DigiCertCustomFieldDataType.Text => "text", + DigiCertCustomFieldDataType.Int => "int", + DigiCertCustomFieldDataType.EmailAddress => "email_address", + DigiCertCustomFieldDataType.EmailList => "email_list", + _ => null + }; + } + } +} \ No newline at end of file diff --git a/digicert-metadata-sync/Logic/ListFlushExtensions.cs b/digicert-metadata-sync/Logic/ListFlushExtensions.cs new file mode 100644 index 0000000..6fb8e4d --- /dev/null +++ b/digicert-metadata-sync/Logic/ListFlushExtensions.cs @@ -0,0 +1,52 @@ +using NLog; +using System.Runtime.CompilerServices; + + +namespace DigicertMetadataSync.Logic +{ + public static class ListFlushExtensions + { + /// + /// Add one item; if the buffer hits , write every item to trace, + /// add to the running total, and clear the buffer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AddAndMaybeFlush(this List buffer, + string item, + Logger log, + string label, + ref int totalCount, + int threshold = 1000) + { + buffer.Add(item); + if (buffer.Count >= threshold) + FlushToTrace(buffer, log, label, ref totalCount); + } + + /// + /// Flush any remaining items after a page/loop ends. + /// + public static void FlushRemainder(this List buffer, + Logger log, + string label, + ref int totalCount) + => FlushToTrace(buffer, log, label, ref totalCount); + + private static void FlushToTrace(List buffer, + Logger log, + string label, + ref int totalCount) + { + if (buffer.Count == 0) return; + + // One line per item keeps logs searchable and prevents jumbo lines. + foreach (var s in buffer) + log.Trace("{Label}: {Item}", label, s); + + totalCount += buffer.Count; + buffer.Clear(); // release memory + buffer.TrimExcess(); // optional: shrink backing array + } + } + +} diff --git a/digicert-metadata-sync/Logic/ValueCoercion.cs b/digicert-metadata-sync/Logic/ValueCoercion.cs new file mode 100644 index 0000000..96a44ef --- /dev/null +++ b/digicert-metadata-sync/Logic/ValueCoercion.cs @@ -0,0 +1,272 @@ +// Copyright 2024 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. +using System.Globalization; +using System.Text.Json; +using System.Text.RegularExpressions; +using DigicertMetadataSync.Models; + +public static class ValueCoercion +{ + // --- ADD: configurable Keyfactor date format (default keeps current behavior) --- + private const string DefaultKfDateFormat = "yyyy-MM-dd"; + private static string _kfDateFormat = DefaultKfDateFormat; + /// + /// Output format for Keyfactor Date metadata (e.g., "M/d/yyyy h:mm:ss tt"). + /// Set this once at startup from config. Null/empty resets to default "yyyy-MM-dd". + /// + public static string KeyfactorDateFormat + { + get => _kfDateFormat; + set => _kfDateFormat = string.IsNullOrWhiteSpace(value) ? DefaultKfDateFormat : value!; + } + + private static readonly Regex EmailRx = new(@"^[A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,}$", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + // ---- public API ---- + + public static object? Coerce(JsonElement value, + KeyfactorMetadataDataType type, + string[]? options) + { + return type switch + { + KeyfactorMetadataDataType.Integer => CoerceInt(value), + KeyfactorMetadataDataType.Boolean => CoerceBool(value), + KeyfactorMetadataDataType.Date => CoerceDateYmd(value), + KeyfactorMetadataDataType.Email => CoerceEmailCsv(value), + KeyfactorMetadataDataType.MultipleChoice => CoerceChoice(value, options), + KeyfactorMetadataDataType.BigText => CoerceString(value, true), + KeyfactorMetadataDataType.String => CoerceString(value), + _ => CoerceString(value) + }; + } + + // Back-compat overload: accepts numeric code (e.g., field.KeyfactorDataTypeCode) + public static object? Coerce(JsonElement value, + int keyfactorTypeCode, + string[]? options) + { + var type = Enum.IsDefined(typeof(KeyfactorMetadataDataType), keyfactorTypeCode) + ? (KeyfactorMetadataDataType)keyfactorTypeCode + : KeyfactorMetadataDataType.String; + + return Coerce(value, type, options); + } + + // ---- scalars ---- + + private static int? CoerceInt(JsonElement v) + { + switch (v.ValueKind) + { + case JsonValueKind.Number: + if (v.TryGetInt32(out var n)) return n; + if (v.TryGetInt64(out var l)) + { + if (l > int.MaxValue || l < int.MinValue) return null; + return (int)l; + } + + return null; + + case JsonValueKind.String: + var s = v.GetString(); + if (string.IsNullOrWhiteSpace(s)) return null; + if (int.TryParse(s.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var i)) + return i; + return null; + + case JsonValueKind.True: return 1; + case JsonValueKind.False: return 0; + default: return null; + } + } + + private static bool? CoerceBool(JsonElement v) + { + return v.ValueKind switch + { + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Number => v.TryGetInt32(out var n) ? n != 0 : null, + JsonValueKind.String => ParseBoolLoose(v.GetString()), + _ => null + }; + } + + private static string? CoerceDateYmd(JsonElement v) + { + // Accept ISO 8601 or y/M/d etc., emit in configurable Keyfactor format + if (TryExtractString(v, out var s) && !string.IsNullOrWhiteSpace(s)) + if (DateTimeOffset.TryParse(s, CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, + out var dto)) + return dto.ToString(_kfDateFormat, CultureInfo.InvariantCulture); + + // If it's already yyyy-MM-dd but config requests a different format, reformat + if (v.ValueKind == JsonValueKind.String) + { + var raw = v.GetString(); + if (!string.IsNullOrWhiteSpace(raw) && Regex.IsMatch(raw!, @"^\d{4}-\d{2}-\d{2}$")) + { + if (_kfDateFormat == DefaultKfDateFormat) + return raw; + + if (DateTime.TryParseExact(raw, DefaultKfDateFormat, CultureInfo.InvariantCulture, + DateTimeStyles.None, out var dt)) + return dt.ToString(_kfDateFormat, CultureInfo.InvariantCulture); + } + } + + return null; + } + + private static string? CoerceString(JsonElement v, bool multiline = false) + { + switch (v.ValueKind) + { + case JsonValueKind.String: + var s = v.GetString(); + return string.IsNullOrWhiteSpace(s) ? null : s; + + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + return v.GetRawText(); // culture-invariant + + case JsonValueKind.Array: + // join array items with ", " for display fields + var parts = v.EnumerateArray() + .Select(e => CoerceString(e)) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .ToArray(); + return parts.Length == 0 ? null : string.Join(", ", parts); + + case JsonValueKind.Object: + // For BigText allow JSON snapshot if that's what you want to store + return multiline ? v.GetRawText() : null; + + default: + return null; + } + } + + // ---- special types ---- + + private static string? CoerceEmailCsv(JsonElement v) + { + var all = ExtractEmails(v); + if (all.Count == 0) return null; + + var normalized = all.Select(x => x.Trim()) + .Where(x => x.Length > 0) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + return normalized.Length == 0 ? null : string.Join(", ", normalized); + } + + private static object? CoerceChoice(JsonElement v, string[]? options) + { + // Keyfactor MultipleChoice expects a single selected option (string). + var val = CoerceString(v); + if (string.IsNullOrWhiteSpace(val)) + return null; + + if (options is null || options.Length == 0) + return val; // nothing to validate against + + // Normalize then match case-insensitively + var norm = NormalizeChoice(val); + var match = options.FirstOrDefault(o => NormalizeChoice(o) == norm); + return match ?? null; // invalid value -> null so you don't submit a bad choice + } + + // ---- helpers ---- + + private static bool? ParseBoolLoose(string? s) + { + if (string.IsNullOrWhiteSpace(s)) return null; + s = s.Trim().ToLowerInvariant(); + return s switch + { + "true" or "t" or "yes" or "y" or "1" => true, + "false" or "f" or "no" or "n" or "0" => false, + _ => null + }; + } + + private static bool TryExtractString(JsonElement v, out string? s) + { + switch (v.ValueKind) + { + case JsonValueKind.String: + s = v.GetString(); + return true; + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + s = v.GetRawText(); + return true; + default: + s = null; + return false; + } + } + + private static string NormalizeChoice(string s) + { + return Regex.Replace(s.Trim(), @"\s+", " ").ToLowerInvariant(); + } + + private static List ExtractEmails(JsonElement v) + { + var outList = new List(8); + + void FromText(string? text) + { + if (string.IsNullOrWhiteSpace(text)) return; + + var pieces = text.Split(new[] { ',', ';', ' ', '\t', '\r', '\n' }, + StringSplitOptions.RemoveEmptyEntries); + foreach (var raw in pieces) + { + var candidate = raw.Trim().Trim('"', '\'', '<', '>', '(', ')', '[', ']'); + if (EmailRx.IsMatch(candidate)) + outList.Add(candidate); + } + } + + switch (v.ValueKind) + { + case JsonValueKind.String: + FromText(v.GetString()); + break; + + case JsonValueKind.Array: + foreach (var item in v.EnumerateArray()) + if (item.ValueKind == JsonValueKind.String) FromText(item.GetString()); + else if (item.ValueKind == JsonValueKind.Object && item.TryGetProperty("email", out var one) && + one.ValueKind == JsonValueKind.String) + FromText(one.GetString()); + break; + + case JsonValueKind.Object: + if (v.TryGetProperty("email", out var e) && e.ValueKind == JsonValueKind.String) + FromText(e.GetString()); + else if (v.TryGetProperty("emails", out var es)) outList.AddRange(ExtractEmails(es)); + else FromText(v.GetRawText()); + break; + + default: + FromText(v.GetRawText()); + break; + } + + return outList; + } +} \ No newline at end of file diff --git a/digicert-metadata-sync/Logic/ValueCoercionDC.cs b/digicert-metadata-sync/Logic/ValueCoercionDC.cs new file mode 100644 index 0000000..b167538 --- /dev/null +++ b/digicert-metadata-sync/Logic/ValueCoercionDC.cs @@ -0,0 +1,72 @@ +// Copyright 2024 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. +using System.Text.RegularExpressions; +using DigicertMetadataSync.Models; + +namespace DigicertMetadataSync.Logic; + +public class ValueCoercionDC +{ + private static readonly Regex EmailRx = new(@"^[A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,}$", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + public static string? CoerceForDigiCert(string? value, + DigiCertCustomFieldDataType dcType, + string[]? kfOptions = null) + { + if (value is null) return null; + + // Common normalize + var s = value.Trim(); + if (s.Length == 0) return null; + + switch (dcType) + { + case DigiCertCustomFieldDataType.Int: + // Accept int-like strings only + return int.TryParse(s, out var n) ? n.ToString() : null; + + case DigiCertCustomFieldDataType.EmailAddress: + return EmailRx.IsMatch(s) ? s : null; + + case DigiCertCustomFieldDataType.EmailList: + { + // split on , ; whitespace, validate each, return CSV (", ") + var emails = s.Split(new[] { ',', ';', ' ', '\t', '\r', '\n' }, + StringSplitOptions.RemoveEmptyEntries) + .Select(e => e.Trim().Trim('"', '\'', '<', '>', '(', ')', '[', ']')) + .Where(e => EmailRx.IsMatch(e)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + return emails.Length == 0 ? null : string.Join(",", emails); + } + + case DigiCertCustomFieldDataType.Text: + // If KF field was MultipleChoice and you provided options, normalize to the canonical option text + if (kfOptions is { Length: > 0 }) + { + var norm = NormalizeChoice(s); + var match = kfOptions.FirstOrDefault(o => NormalizeChoice(o) == norm); + return match ?? s; // fall back to raw text if not matched + } + + return s; + + case DigiCertCustomFieldDataType.Anything: + // Pass through; DigiCert treats omitted data_type as free text + return s; + + default: + return s; + } + } + + private static string NormalizeChoice(string x) + { + return Regex.Replace(x.Trim(), @"\s+", " ").ToLowerInvariant(); + } +} \ No newline at end of file diff --git a/digicert-metadata-sync/MetadataSync.cs b/digicert-metadata-sync/MetadataSync.cs index ce31580..c2715e6 100644 --- a/digicert-metadata-sync/MetadataSync.cs +++ b/digicert-metadata-sync/MetadataSync.cs @@ -1,588 +1,651 @@ -// Copyright 2021 Keyfactor +// Copyright 2024 Keyfactor // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions // and limitations under the License. -using System.Collections.Generic; -using System.IO; -using System.Reflection.Metadata.Ecma335; +using System.Text.Json; +using DigicertMetadataSync.Client; +using DigicertMetadataSync.Logic; +using DigicertMetadataSync.Models; using Microsoft.Extensions.Configuration; -using Newtonsoft.Json; +using Microsoft.Extensions.DependencyInjection; using NLog; -using RestSharp; -using RestSharp.Authenticators; -using ConfigurationManager = System.Configuration.ConfigurationManager; -//using Keyfactor.Logging; +using NLog.Config; namespace DigicertMetadataSync; -internal partial class DigicertSync +internal class DigicertSync { // create a static _logger field private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); public static void Main(string[] args) { - _logger.Debug("Start sync"); - var digicertapikey = ConfigurationManager.AppSettings.Get("DigicertAPIKey"); - var digicertapikeytopperm = ConfigurationManager.AppSettings.Get("DigicertAPIKeyTopPerm"); - var keyfactorusername = ConfigurationManager.AppSettings.Get("KeyfactorDomainAndUser"); - var keyfactorpassword = ConfigurationManager.AppSettings.Get("KeyfactorPassword"); - var importdeactivated = Convert.ToBoolean(ConfigurationManager.AppSettings.Get("ImportDataForDeactivatedDigiCertFields")); - int batchsize = 200; - var importallcustomdigicertfields = - Convert.ToBoolean(ConfigurationManager.AppSettings.Get("ImportAllCustomDigicertFields")); - _logger.Debug("Settings: importallcustomdigicertfields={0}, replacementcharacter={1}", - importallcustomdigicertfields); - var config_mode = args[0]; - if (CheckMode(config_mode) == false) + // Define the config directory path + var configDirectory = Path.Combine(Directory.GetCurrentDirectory(), "config"); + + // Ensure the config directory exists + if (!Directory.Exists(configDirectory)) Directory.CreateDirectory(configDirectory); + // Set up NLog to load the configuration from the config folder + var nlogConfigPath = Path.Combine(configDirectory, "nlog.config"); + if (File.Exists(nlogConfigPath)) + LogManager.Configuration = new XmlLoggingConfiguration(nlogConfigPath); + else + _logger.Error($"NLog configuration file not found at {nlogConfigPath}. Using default configuration."); + + // Start of the run + var runId = Guid.NewGuid(); + _logger.Info("============================================================"); + _logger.Info($"[START] DigiCert Metadata Sync - Run at {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); + _logger.Info($"[RUN ID: {runId}]"); + _logger.Info("============================================================"); + /////////////////////////// + // SECTION I: Initial setup and connection testing + _logger.Debug("Loading configuration."); + ConfigMode configMode; + try { - _logger.Error("Inappropriate configuration mode. Check your command line arguments."); - throw new Exception("Inappropriate configuration mode. Check your command line arguments."); - } + if (args.Length == 0) + throw new ArgumentException("No configuration mode provided. Please specify KFtoSC or SCtoKF."); - var digicertIssuerQueryterm = ConfigurationManager.AppSettings.Get("KeyfactorDigicertIssuedCertQueryTerm"); - var returnlimit = ConfigurationManager.AppSettings.Get("KeyfactorCertSearchReturnLimit"); - var keyfactorapilocation = ConfigurationManager.AppSettings.Get("KeyfactorAPIEndpoint"); - int returnlimitint = Int32.Parse(returnlimit); - int numberOfBatches = (int)Math.Ceiling((double)returnlimitint / batchsize); - _logger.Debug("Loaded config. Starting metadata field name processing."); - - - // Initializing net client - var client = new RestClient(); - client.Authenticator = new HttpBasicAuthenticator(keyfactorusername, keyfactorpassword); - - //Getting list of custom metadata fields from Keyfactor - var getmetadalistkf = keyfactorapilocation + "MetadataFields"; - var getmetadatakfclient = new RestClient(); - getmetadatakfclient.Authenticator = new HttpBasicAuthenticator(keyfactorusername, keyfactorpassword); - var metadatakfrequest = new RestRequest(getmetadalistkf); - metadatakfrequest.AddHeader("Accept", "application/json"); - metadatakfrequest.AddHeader("x-keyfactor-api-version", "1"); - metadatakfrequest.AddHeader("x-keyfactor-requested-with", "APIClient"); - var metadatakfresponse = client.Execute(metadatakfrequest); - var metadatakfrawresponse = metadatakfresponse.Content; - var kfmetadatafields = JsonConvert.DeserializeObject>(metadatakfrawresponse); - Console.WriteLine("Got list of custom fields from Keyfactor."); - _logger.Debug("Got list of custom fields from Keyfactor."); - - //Getting list of custom metadata fields on DigiCert - var customdigicertmetadatafieldlist = GrabCustomFieldsFromDigiCert(digicertapikey, importdeactivated); - - //Convert DigiCert custom fields to Keyfactor appropriate ones - //This depends on whether the setting to import all fields was enabled or not - - var config = new ConfigurationBuilder().SetBasePath(AppDomain.CurrentDomain.BaseDirectory) - .AddJsonFile("manualfields.json").Build(); - var kfcustomfields = new List(); - - if (importallcustomdigicertfields) - { - _logger.Debug("Loading custom fields using autofill"); - //This imports all the custom fields based on the list of metadata from DigiCert and does autofill - for (var i = 0; i < customdigicertmetadatafieldlist.Count; i++) + // Parse the config mode from the command-line arguments + if (!Enum.TryParse(args[0], true, out configMode)) { - var localkffieldinstance = new ReadInMetadataField(); - var kfdatatype = "String"; - if (customdigicertmetadatafieldlist[i].data_type != null) - localkffieldinstance.KeyfactorDataType = customdigicertmetadatafieldlist[i].data_type; - else - localkffieldinstance.KeyfactorDataType = "String"; - if (customdigicertmetadatafieldlist[i].label != null) - { - /* - NOTICE: KEYFACTOR DOES NOT SUPPORT SPACES IN METADATA FIELD NAMES. - WHITESPACE MUST BE REMOVED FROM THE NAME. - CURRENTLY REPLACING WITH "_-_" AS STAND IN FOR SPACE CHARACTER. - */ - localkffieldinstance.DigicertFieldName = customdigicertmetadatafieldlist[i].label; - localkffieldinstance.KeyfactorMetadataFieldName = customdigicertmetadatafieldlist[i].label; - _logger.Debug("DC field name {0} becomes {1} in Keyfactor", localkffieldinstance.DigicertFieldName, - localkffieldinstance.KeyfactorMetadataFieldName); - } - else - { - localkffieldinstance.DigicertFieldName = ""; - localkffieldinstance.KeyfactorMetadataFieldName = ""; - } - - if (customdigicertmetadatafieldlist[i].description != null) - localkffieldinstance.KeyfactorDescription = customdigicertmetadatafieldlist[i].description; - else - localkffieldinstance.KeyfactorDescription = "None."; - - localkffieldinstance.KeyfactorAllowAPI = "True"; - localkffieldinstance.KeyfactorHint = ""; - //Other parameters like enrollment can be set here too. - - kfcustomfields.Add(localkffieldinstance); + _logger.Error("Invalid configuration mode. Please specify KFtoDC or DCtoKF."); + throw new ArgumentException("Invalid configuration mode. Please specify KFtoDC or DCtoKF."); } } - else - { - // This loads custom metadata using the manualfields config. - // Converts blank fields etc and preps the data. - var customfieldslst = "CustomFields"; - kfcustomfields = config.GetSection(customfieldslst).Get>(); - if (kfcustomfields == null) kfcustomfields = new List(); - _logger.Debug("Loading custom fields using json, no autofill/conversion"); - } - foreach (var item in kfcustomfields) + catch (Exception ex) { - item.FieldType = "Custom"; + _logger.Error($"Unable to process tool mode: {ex.Message}"); + throw; // Use 'throw;' to preserve the original stack trace } + _logger.Info($"Configuration mode set to: {configMode}"); - //Adding metadata fields for the ID and the email of the requester from DigiCert. - var kfmanualfields = new List(); - var manualfieldslist = "ManualFields"; - kfmanualfields = config.GetSection(manualfieldslist).Get>(); - if (kfmanualfields == null) kfmanualfields = new List(); - foreach (var item in kfmanualfields) + // Build the config + var config = new ConfigurationBuilder().Build(); + try { - item.FieldType = "Manual"; + config = new ConfigurationBuilder() + .SetBasePath(configDirectory) // Set the base path to the config directory + .AddJsonFile("config.json", false, false) + .AddJsonFile("fields.json", false, false) + .AddJsonFile("bannedcharacters.json", false, false) + .Build(); } - _logger.Debug("Performed field conversion."); - - //Pulling list of existing metadata fields from Keyfactor for later comparison. - var noexistingfields = true; - - var existingmetadataurl = keyfactorapilocation + "MetadataFields"; - var existingmetadataclient = new RestClient(); - existingmetadataclient.Authenticator = new HttpBasicAuthenticator(keyfactorusername, keyfactorpassword); - var existingmetadatareq = new RestRequest(existingmetadataurl); - existingmetadatareq.AddHeader("Accept", "application/json"); - existingmetadatareq.AddHeader("x-keyfactor-api-version", "1"); - existingmetadatareq.AddHeader("x-keyfactor-requested-with", "APIClient"); - var existingmetadataresponse = existingmetadataclient.Execute(existingmetadatareq); - var existingmetadatalist = new List(); - if (existingmetadataresponse != null) + catch (Exception ex) { - //Fields exist - existingmetadatalist = - JsonConvert.DeserializeObject>(existingmetadataresponse.Content); - noexistingfields = false; + _logger.Error($"Unable to load config file: {ex.Message}"); + throw; // Use 'throw;' to preserve the original stack trace } - Console.WriteLine("Pulled existing metadata fields from keyfactor."); - _logger.Debug("Pulled existing metadata fields from Keyfactor."); + Config settings = new(); + List bannedCharList = new(); - - - // Carrying out the persistent banned character database check - // Loading up the character database - string currentDirectory = Directory.GetCurrentDirectory(); - - string filePath = Path.Combine(currentDirectory, "replacechar.json"); - - bool restartandconfigrequired = false; - - List allBannedChars = JsonConvert.DeserializeObject>(File.ReadAllText(filePath)); - - if (importallcustomdigicertfields) + try { - CheckForChars(kfmanualfields, allBannedChars, restartandconfigrequired); - CheckForChars(kfcustomfields, allBannedChars, restartandconfigrequired); + // Required: Config + settings = config.GetSection("Config") + .Get() + ?? throw new InvalidOperationException("Missing config section in the config json file."); + + // Optional: ManualFields & CustomFields (empty list is fine) + _ = config.GetSection("ManualFields") + .Get>(o => o.ErrorOnUnknownConfiguration = true) ?? + new List(); + + _ = config.GetSection("CustomFields") + .Get>(o => o.ErrorOnUnknownConfiguration = true) ?? + new List(); + + // Optional: BannedCharacters (default to empty list) + bannedCharList = config.GetSection("BannedCharacters") + .Get>(o => o.ErrorOnUnknownConfiguration = true) ?? new List(); + } + catch (Exception ex) + { + _logger.Error($"Unable to process config file: {ex.Message}"); + throw; // Use 'throw;' to preserve the original stack trace + } - string formattedjsonchars = JsonConvert.SerializeObject(allBannedChars, Formatting.Indented); - File.WriteAllText(filePath, formattedjsonchars); + ValueCoercion.KeyfactorDateFormat = settings.keyfactorDateFormat; + _logger.Info("Configuration loaded successfully. Testing connection to DigiCert API and Keyfactor API."); + + + // Setup the service + var services = new ServiceCollection(); + services.AddDigiCertClient("https://www.digicert.com/services/v2/"); + services.AddKeyfactorMetadataClient(settings.keyfactorAPIUrl); + // Build the service provider + var provider = services.BuildServiceProvider(); + + // Set up and authenticate DigiCert clients + var dcApiKeyClient = provider.GetRequiredService(); + dcApiKeyClient.Authenticate(settings.digicertApiKey); + // Test connection + var dcFields = dcApiKeyClient.ListCustomFields(); + + // Test Keyfactor connection + var kfClient = provider.GetRequiredService(); + // Authenticate + kfClient.Authenticate( + settings.keyfactorDomainAndUser, + settings.keyfactorPassword + ); + var kfFields = new List(); + try + { + kfFields = kfClient.ListMetadataFields(); + _logger.Debug("Retrieved All Metadata Fields from Keyfactor."); + } + catch (Exception ex) + { + _logger.Error($"Failed to connect to Keyfactor API: {ex.Message}"); + _logger.Fatal($"Critical error: {ex.Message}"); + Environment.Exit(1); // Exit with a non-zero code to indicate failure + throw; // Use 'throw;' to preserve the original stack trace + } - foreach (var badchar in allBannedChars) + ///////////// + //SECTION II: Determination of field overlap + var unifiedFieldList = new List(); + try + { + if (settings.importAllCustomDigicertFields) { - if (badchar.replacementcharacter == "null") - { - restartandconfigrequired = true; - break; - } - } + _logger.Info( + "importAllCustomDigicertFields is enabled. Mapping DigiCert custom fields to UnifiedFormatField. Notice: fields that have options will not have these options added" + + "to Keyfactor as DigiCert offers no way to retrieve options (for dropdownmenu/email list). Please consider using fields.json if you want to enable options for any metadata fields in Keyfactor."); + unifiedFieldList = dcFields + // Include disabled only if explicitly enabled; treat null as active + .Where(f => settings.importDataForDeactivatedDigiCertFields || (f.IsActive == true)) + .Select(f => + { + var name = !string.IsNullOrWhiteSpace(f.Label) ? f.Label! : $"metadata_{f.Id}"; + var dcType = f.DataType; // may be null; your mapper should handle it - if (restartandconfigrequired) - { - _logger.Trace("Please replace \"null\" with your desired replacement characters in replacechar.json and re-run the tool! Only alphanumerics, \"-\" and \"_\" are allowed"); - Console.WriteLine("Please replace \"null\" with your desired replacement characters in replacechar.json and re-run the tool! Only alphanumerics, \"-\" and \"_\" are allowed"); - Environment.Exit(0); + return new UnifiedFormatField + { + // Names/descriptions (no sanitization) + DigicertFieldName = name, + KeyfactorMetadataFieldName = name, + KeyfactorDescription = name, + // Type mapping via your DigiCert -> Keyfactor mapper + KeyfactorDataType = Helpers.ToKeyfactorDataType(dcType), + // No validation/message per request + KeyfactorHint = string.Empty, + KeyfactorValidation = string.Empty, + KeyfactorMessage = string.Empty, + // Required -> enrollment required + KeyfactorEnrollment = f.IsRequired == true ? 1 : 0, + // DigiCert account metadata API doesn't return option sets + KeyfactorOptions = [], + KeyfactorAllowAPI = true, + // Defaults + KeyfactorDefaultValue = string.Empty, + KeyfactorDisplayOrder = 0, + KeyfactorCaseSensitive = false, + KeyfactorMetadataFieldId = 0, + ToolFieldType = UnifiedFieldType.Custom + }; + }) + .ToList(); + + _logger.Info($"Loaded {unifiedFieldList.Count} custom fields from DigiCert."); } + else + { + _logger.Info("importAllCustomDigicertFields is disabled. Using field mapping from configuration."); + // This loads custom metadata using the CustomFields config. - + unifiedFieldList = config + .GetSection("CustomFields") + .Get>(o => o.ErrorOnUnknownConfiguration = true); + foreach (var item in unifiedFieldList) item.ToolFieldType = UnifiedFieldType.Custom; + } + } + catch (InvalidOperationException ex) + { + _logger.Fatal($"Critical error: {ex.Message}"); + Environment.Exit(1); + } + catch (Exception ex) + { + _logger.Error($"Error processing custom fields: {ex.Message}"); } - // Converting the read in fields into sendable lists - var convertedmanualfields = convertlisttokf(kfmanualfields, allBannedChars, importallcustomdigicertfields); - var convertedcustomfields = convertlisttokf(kfcustomfields, allBannedChars, importallcustomdigicertfields); - - + _logger.Debug($"Loaded {unifiedFieldList.Count.ToString()} Custom Fields."); - _logger.Trace("Sending following manual fields to KF: {0}", JsonConvert.SerializeObject(convertedmanualfields)); - var totalfieldsadded = 0; + // Load the manual fields from the config file and add it to the field list. + var unifiedManualFieldList = config.GetSection("ManualFields") + .Get>(o => o.ErrorOnUnknownConfiguration = true)? + .Select(item => + { + item.ToolFieldType = UnifiedFieldType.Manual; + return item; + }) + .ToList() ?? new List(); + unifiedFieldList.AddRange(unifiedManualFieldList); + _logger.Debug($"Loaded {unifiedManualFieldList.Count.ToString()} Manual Fields."); - //If all the fields are absent from Keyfactor, the fields are added. - var manualresult = AddFieldsToKeyfactor(convertedmanualfields, existingmetadatalist, noexistingfields, - keyfactorusername, keyfactorpassword, keyfactorapilocation); - _logger.Trace("Sending following custom fields to KF: {0}", JsonConvert.SerializeObject(convertedcustomfields)); + // Initialize a list to collect invalid character details + var invalidCharacterDetails = new List(); - var customresult = AddFieldsToKeyfactor(convertedcustomfields, existingmetadatalist, noexistingfields, - keyfactorusername, keyfactorpassword, keyfactorapilocation); + // Check both lists for bad characters, ask for restart if needed. + var restartRequired = false; - totalfieldsadded += manualresult.Item1; - totalfieldsadded += customresult.Item1; + if (settings.importAllCustomDigicertFields) + BannedCharacters.CheckForChars(unifiedFieldList, bannedCharList, invalidCharacterDetails); + else + BannedCharacters.CheckForChars(unifiedFieldList, bannedCharList, invalidCharacterDetails, true); - var allnewfields = manualresult.Item2.Concat(customresult.Item2).ToList(); + foreach (var badchar in bannedCharList) + if (badchar.replacementcharacter == "null") + restartRequired = true; + // Serialize the banned characters list with pretty-printing + var formattedCharList = JsonSerializer.Serialize(new { BannedCharacters = bannedCharList }, + new JsonSerializerOptions + { + WriteIndented = true // Enable pretty-printing + }); - //Processing this batch - Console.WriteLine($"Added custom fields to Keyfactor. Total fields added: {totalfieldsadded.ToString()}"); - _logger.Debug($"Added custom fields to Keyfactor. Total fields added: {totalfieldsadded.ToString()}"); + File.WriteAllText(Path.Combine(configDirectory, "bannedcharacters.json"), formattedCharList); - // Syncing Data from Keyfactor TO DigiCert - // Sync from DigiCert to Keyfactor must run at least once prior to this - only runs with custom fields - if (config_mode == "kftodc") + // Log aggregated invalid character details if replacements are missing + if (restartRequired && invalidCharacterDetails.Any()) { - // Initialize variable to keep track of items downloaded so far - int certsdownloaded = 0; - var certcounttracker = 0; - var totalcertsprocessed = 0; - var numcertsdatauploaded = 0; - for (int batchnum = 0; batchnum < numberOfBatches; batchnum++) - { - // Check if reaching the arbitrary limit - if (certsdownloaded + batchsize > returnlimitint) - { - Console.WriteLine($"Stopped downloading at the configured limit of {returnlimitint} items."); - _logger.Debug($"Stopped downloading at the configured limit of {returnlimitint} items."); - break; - } - + _logger.Warn("The following fields contain invalid characters with no replacements:"); + foreach (var detail in invalidCharacterDetails) _logger.Warn(detail); + } - var fullcustomdgfieldlist = new List(); - var newcustomfieldsfordg = new List(); - - // Download the items in this batch - var digicertlookup = keyfactorapilocation + "Certificates?pq.queryString=IssuerDN%20-contains%20%22" - + digicertIssuerQueryterm + "%22&pq.returnLimit=" + batchsize.ToString() + - "&includeMetadata=true" + "&pq.pageReturned=" + batchnum.ToString(); - var request = new RestRequest(digicertlookup); - request.AddHeader("Accept", "application/json"); - request.AddHeader("x-keyfactor-api-version", "1"); - request.AddHeader("x-keyfactor-requested-with", "APIClient"); - var response = client.Execute(request); - var rawresponse = response.Content; - var certlist = JsonConvert.DeserializeObject>(rawresponse, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); - Console.WriteLine("Got DigiCert issued certs from keyfactor"); - _logger.Debug("Got DigiCert issued certs from keyfactor"); - - // Rebuild the list of metadata field names as they are on DigiCerts side. - - // This covers all of the custom fields on Digicerts side - foreach (var dgcustomfield in customdigicertmetadatafieldlist) - { - var localdigicertfieldinstance = new DigicertCustomFieldInstance(); + if (restartRequired) + { + // Tool needs restarting at this point. + var bannedChars = new Exception("Replacement characters for auto-fill for automated DigiCert custom field import need specifying. Please fill in the required data in config/bannedcharacters.json."); + _logger.Fatal($"Critical error: {bannedChars.Message}"); + Environment.Exit(1); // Exit with a non-zero code to indicate failure + } - localdigicertfieldinstance.label = dgcustomfield.label; - localdigicertfieldinstance.is_active = dgcustomfield.is_active; - localdigicertfieldinstance.data_type = dgcustomfield.data_type; - localdigicertfieldinstance.is_required = dgcustomfield.is_required; + // Process the fields - run banned character replacement and send the fields off to Keyfactor. + Parallel.ForEach(unifiedFieldList, + field => + { + field.KeyfactorMetadataFieldName = + BannedCharacters.ReplaceAllBannedCharacters(field.KeyfactorMetadataFieldName, bannedCharList); + }); + kfClient.SendUnifiedMetadataFields(unifiedFieldList, kfFields); - foreach (var kffieldeq in kfcustomfields) - if (dgcustomfield.label == kffieldeq.DigicertFieldName) - localdigicertfieldinstance.kf_field_name = kffieldeq.DigicertFieldName; + // Loading DigiCert metadata field IDs into the unified list (for custom fields only) + foreach (var unifiedField in unifiedFieldList) + { + var matchingDcField = dcFields + .FirstOrDefault(dc => + string.Equals(dc.Label, unifiedField.DigicertFieldName, + StringComparison.OrdinalIgnoreCase)); + if (matchingDcField != null) + { + unifiedField.DigiCertMetadaFieldId = matchingDcField.Id; + unifiedField.DigicertDataType = Helpers.ToDigiCertEnumFromString(matchingDcField.DataType); + } - fullcustomdgfieldlist.Add(localdigicertfieldinstance); - } + } + //Step 1 - pull all digicert custom fields + // step 2 - if some if these fields do not exist in keyfactor + //if importAllCustomDigicertFields = true put out a message stating they dont exist in digicert and need to be created manually, will be synced on next run. + //else create them - //This covers all of the new fields on Keyfactors side, including new ones - needs to have digicert ids for the new ones - foreach (var kfcustomfield in kfcustomfields) + //If running in KFtoDC mode, we need to update the field IDs in the unified list. + if (configMode == ConfigMode.KFtoDC) + if (settings.createMissingFieldsInDigicert) + { + // STEP 1 - Identify what fields need to get pushed to digicert. + var customFieldsOnly = unifiedFieldList.Where(f => f.ToolFieldType == UnifiedFieldType.Custom) + .ToList(); + var fieldsNotInDC = customFieldsOnly + .Where(customField => !dcFields + .Any(dcField => string.Equals(customField.DigicertFieldName, dcField.Label, + StringComparison.OrdinalIgnoreCase))) + .ToList(); + // STEP 2 - Push fields to digicert + if (fieldsNotInDC.Count > 0) { - var localdigicertfieldinstance = new DigicertCustomFieldInstance(); - localdigicertfieldinstance.label = kfcustomfield.DigicertFieldName; - localdigicertfieldinstance.is_active = true; - localdigicertfieldinstance.kf_field_name = kfcustomfield.KeyfactorMetadataFieldName; - if (kfcustomfield.KeyfactorDataType == "String") - localdigicertfieldinstance.data_type = "text"; - else if (kfcustomfield.KeyfactorDataType == "Int") - localdigicertfieldinstance.data_type = "int"; - else - localdigicertfieldinstance.data_type = "anything"; - localdigicertfieldinstance.is_required = false; - - if (!fullcustomdgfieldlist.Any(p => p.label == localdigicertfieldinstance.label)) + if (settings.importAllCustomDigicertFields) { - fullcustomdgfieldlist.Add(localdigicertfieldinstance); - newcustomfieldsfordg.Add(localdigicertfieldinstance); + _logger.Info( + "You are operating KFtoDC with importAllCustomDigicertFields set to true in config.json and have fields that may not exist in DigiCert. Fields that do not have names exactly matched between Keyfactor and DigiCert will not" + + "have their contents synced. If you have fields that exist in Keyfactor but do not exist in DigiCert and you wish to sync their contents to DigiCert," + + "you will need to create these fields manually in DigiCert to sync them during the next run of the tool, or switch to using fields.json."); } - } - - //Add fields that don't exist on DigiCert to Digicert - _logger.Trace("Adding following fields to DigiCert: {0}", - JsonConvert.SerializeObject(newcustomfieldsfordg)); - foreach (var newdgfield in newcustomfieldsfordg) - { - var digicertapilocation = "https://www.digicert.com/services/v2/account/metadata"; - var digicertnewfieldsclient = new RestClient(); - var digicertnewfieldsrequest = new RestRequest(digicertapilocation); - digicertnewfieldsrequest.AddHeader("Accept", "application/json"); - digicertnewfieldsrequest.AddHeader("X-DC-DEVKEY", digicertapikeytopperm); - var serializedsyncfield = JsonConvert.SerializeObject(newdgfield); - digicertnewfieldsrequest.AddParameter("application/json", serializedsyncfield, - ParameterType.RequestBody); - var digicertresponsenewfields = digicertnewfieldsclient.Post(digicertnewfieldsrequest); - } - - - // Grabbing the list again from digicert, populating ids for new ones - //Getting list of custom metadata fields on DigiCert - var updatedmetadatafieldlist = GrabCustomFieldsFromDigiCert(digicertapikey, importdeactivated); - foreach (var subitem in updatedmetadatafieldlist) - foreach (var fulllistitem in fullcustomdgfieldlist) - if (subitem.label == fulllistitem.label) - fulllistitem.id = subitem.id; - - - // Pushing the data to DigiCert - var certlist2 = JsonConvert.DeserializeObject(rawresponse, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); - foreach (var cert in certlist2) - { - Dictionary kfstoredmetadata = - cert["Metadata"].ToObject>(); - - var certhascustomfields = false; - foreach (var checkfield in fullcustomdgfieldlist) - if (kfstoredmetadata.ContainsKey(checkfield.kf_field_name)) - certhascustomfields = true; - - if (certhascustomfields) + else { - var kfserialnumber = cert["SerialNumber"].ToString(); - - var digicertnewlookupurl = "https://www.digicert.com/services/v2/order/certificate" + - "?filters[serial_number]=" + kfserialnumber; - - var newbodytemplate = new RootDigicertLookup(); - var newsearchcriterioninstance = new SearchCriterion(); - newbodytemplate.searchCriteriaList.Add(newsearchcriterioninstance); - var lookupnewrequest = new RestRequest(digicertnewlookupurl); - lookupnewrequest.AddHeader("Content-Type", "application/json"); - lookupnewrequest.AddHeader("X-DC-DEVKEY", digicertapikey); - var digicertnewlookupresponse = client.Execute(lookupnewrequest); - var newparseddigicertresponse = - JsonConvert.DeserializeObject(digicertnewlookupresponse.Content); - - - if (newparseddigicertresponse["page"]["total"] != 0) + _logger.Info("Creating custom fields in DigiCert to match data in fields.json."); + try { - var newflatteneddigicertinstance = newparseddigicertresponse["orders"][0]; - var orderid = newflatteneddigicertinstance["id"].ToString(); - - var digicertmetadataupdateapilocation = - "https://www.digicert.com/services/v2/order/certificate/" + orderid + "/custom-field"; - var digicertnewfieldsclient = new RestClient(); - var digicertnewfieldsrequest = new RestRequest(digicertmetadataupdateapilocation); - digicertnewfieldsrequest.AddHeader("Accept", "application/json"); - digicertnewfieldsrequest.AddHeader("X-DC-DEVKEY", digicertapikey); - - foreach (var newfield in fullcustomdgfieldlist) + IEnumerable fieldsToCreate = new List(); + foreach (var field in fieldsNotInDC) { - var keyfactorfieldname = ""; - var datauploaded = false; - //Lookup the keyfactor name for digicert fields - foreach (var sublookup in kfcustomfields) - if (sublookup.DigicertFieldName == newfield.label) - { - var metadatapayload = new Dictionary(); - metadatapayload["metadata_id"] = newfield.id.ToString(); - if (kfstoredmetadata.ContainsKey(sublookup.KeyfactorMetadataFieldName)) - { - metadatapayload["value"] = - kfstoredmetadata[sublookup.KeyfactorMetadataFieldName]; - var newserializedsyncfield = JsonConvert.SerializeObject(metadatapayload); - digicertnewfieldsrequest.AddParameter("application/json", - newserializedsyncfield, ParameterType.RequestBody); - var digicertresponsenewfields = - digicertnewfieldsclient.Post(digicertnewfieldsrequest); - datauploaded = true; - } - } + var dcType = field.DigicertDataType != (int)DigiCertCustomFieldDataType.Anything + ? field.DigicertDataType + : Helpers.ToDigiCertDataType(field.KeyfactorDataType); + var (isRequired, isActive) = Helpers.ToDigiCertFlags(field.KeyfactorEnrollment); + + var newField = new DcCustomFieldCreate + { + Label = field.DigicertFieldName, + DataType = dcType.ToWireString(), + IsActive = isActive, + IsRequired = isRequired // Enrollment 1 = Required + // Other properties can be set as needed + }; + fieldsToCreate = fieldsToCreate.Append(newField); } - numcertsdatauploaded += 1; + var createdField = dcApiKeyClient.BulkAddCustomFields(fieldsToCreate); + if (createdField.IsSuccessStatusCode) + _logger.Info( + "Added missing fields to DigiCert."); + } + catch (Exception ex) + { + _logger.Error( + $"Error adding new DigiCert custom field to DigiCert: {ex.Message}"); } - } - totalcertsprocessed += 1; + // STEP 3 - Retrieve and store the new DigiCert field IDs. + dcFields = dcApiKeyClient.ListCustomFields(); + foreach (var unifiedField in unifiedFieldList) + { + var matchingDcField = dcFields + .FirstOrDefault(dc => + string.Equals(dc.Label, unifiedField.DigicertFieldName, + StringComparison.OrdinalIgnoreCase)); + if (matchingDcField != null) unifiedField.DigiCertMetadaFieldId = matchingDcField.Id; + } + } } - - - // Update the count of items downloaded so far - certsdownloaded += batchsize; - - // Check if all items have been downloaded - if (certlist.Count == 0) + else { - Console.WriteLine( - $"Metadata sync from Keyfactor to DigiCert complete. Number of certs processed: {totalcertsprocessed.ToString()}"); - Console.WriteLine($"Certs that had their metadata synced: {numcertsdatauploaded.ToString()}"); - _logger.Debug( - $"Metadata sync from Keyfactor to DigiCert complete. Number of certs processed: {totalcertsprocessed.ToString()}"); - _logger.Debug($"Certs that had their metadata synced: {numcertsdatauploaded.ToString()}"); ; - - break; + _logger.Info("All custom fields are already present in DigiCert. New fields will not be created."); } } - } + else + { + _logger.Debug("Automated field creation for KFtoDC mode is disabled. Fields that exist in fields.json but do not exist in DigiCert will not be added to DigiCert."); + } + - // Syncing Data from DigiCert TO Keyfactor - if (config_mode == "dctokf") + // Get list of all DigiCert Certs stored in Keyfactor. + // Define pagination parameters + var pageSize = settings.keyfactorPageSize; + var pageNumber = 1; + var hasMorePages = true; + + // Initialize counters and lists for tracking certificates + var totalCertsProcessed = 0; + var certsWithoutCustomFields = 0; + + // Initialize cumulative lists for unmatched and successfully updated certificates + var cumulativeUnmatchedCerts = new List(); + int unmatchedCount = 0; + var cumulativePartiallyProcessedCerts = new List(); + int partiallyProcessedCount = 0; + var cumulativeSuccessfullyUpdatedCerts = new List(); + int successfullyUpdatedCount = 0; + + // Initialize a list to collect certificates with missing custom fields + var cumulativeMissingCustomFields = new List(); + int missingCustomFields = 0; + while (hasMorePages) { - // Initialize variable to keep track of items downloaded so far - int certsdownloaded = 0; - var certcounttracker = 0; - for (int batchnum = 0; batchnum < numberOfBatches; batchnum++) + // Get the current page of certificates + var certsPage = kfClient.GetCertificatesByIssuer(settings.keyfactorDigicertIssuedCertQueryTerm, + settings.syncRevokedAndExpiredCerts, pageNumber, pageSize); + if (certsPage.Count > 0) { - // Check if reaching the arbitrary limit - if (certsdownloaded + batchsize > returnlimitint) - { - Console.WriteLine($"Stopped downloading at the configured limit of {returnlimitint} items."); - _logger.Debug($"Stopped downloading at the configured limit of {returnlimitint} items."); - break; - } - - // Download the items in this batch - Console.WriteLine($"Downloading batch {batchnum + 1}..."); - - - var digicertlookup = keyfactorapilocation + "Certificates?pq.queryString=IssuerDN%20-contains%20%22" - + digicertIssuerQueryterm + "%22&pq.returnLimit=" + batchsize.ToString() + - "&includeMetadata=true" + "&pq.pageReturned=" + batchnum.ToString(); - var request = new RestRequest(digicertlookup); - request.AddHeader("Accept", "application/json"); - request.AddHeader("x-keyfactor-api-version", "1"); - request.AddHeader("x-keyfactor-requested-with", "APIClient"); - var response = client.Execute(request); - var rawresponse = response.Content; - var certlist = JsonConvert.DeserializeObject>(rawresponse, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); - Console.WriteLine("Got DigiCert issued certs from keyfactor"); - _logger.Debug("Got DigiCert issued certs from keyfactor"); - - //Each cert that is DigiCert in origin in Keyfactor is looked up on DigiCert via serial number, - //and the metadata contents from those fields are stored. - var digicertlookupclient = new RestClient(); - var digicertcertificates = new List(); - foreach (var certinstance in certlist) + _logger.Info( + $"[PAGE INFO] Retrieved {certsPage.Count} certificates on page {pageNumber}. Processing batch."); + pageNumber++; + // Process the current page of certificates + if (configMode == ConfigMode.DCtoKF) + //Find matching cert by serial number + foreach (var localKfCert in certsPage) { - var digicertlookupurl = "https://www.digicert.com/services/v2/order/certificate/"; - - var bodytemplate = new RootDigicertLookup(); - var searchcriterioninstance = new SearchCriterion(); - bodytemplate.searchCriteriaList.Add(searchcriterioninstance); + var dcResponse = + dcApiKeyClient.GetOrderBySerialOrThumbprint(localKfCert.SerialNumber, + localKfCert.Thumbprint); - digicertlookupurl = digicertlookupurl + certinstance.SerialNumber; - var lookuprequest = new RestRequest(digicertlookupurl); - lookuprequest.AddHeader("Content-Type", "application/json"); - lookuprequest.AddHeader("X-DC-DEVKEY", digicertapikey); - var digicertlookupresponse = client.Execute(lookuprequest); - var certcontent = JsonConvert.DeserializeObject(digicertlookupresponse.Content, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); - if (certcontent["certificate"] != null) - { - digicertcertificates.Add(certcontent); - _logger.Trace("Pulled and storing following cert from digicert: {0}", - digicertlookupresponse.Content); - } - else + if (dcResponse == null) { - _logger.Trace("Failed to retrieve cert {0} from Digicert", digicertlookupresponse.Content); + cumulativeUnmatchedCerts.Add(localKfCert.SerialNumber); + continue; // Skip to the next Keyfactor cert } - } - Console.WriteLine("Pulled DigiCert matching DigiCert cert data."); - _logger.Debug("Pulled DigiCert matching DigiCert cert data."); + // Initialize a flag to track if any fields failed to process + var hasPartialProcessing = false; - foreach (var digicertcertinstance in digicertcertificates) - { - var finalsyncclient = new RestClient(); - finalsyncclient.Authenticator = new HttpBasicAuthenticator(keyfactorusername, keyfactorpassword); - var finalsyncurl = keyfactorapilocation + "Certificates/Metadata"; - //Find matching certificate via Keyfactor ID - var test = digicertcertinstance["certificate"]["serial_number"].ToString().ToUpper(); - var query = from kfcertlocal in certlist - where kfcertlocal.SerialNumber == - digicertcertinstance["certificate"]["serial_number"].ToString().ToUpper() - select kfcertlocal; - var certificateid = query.FirstOrDefault().Id; - - - var payloadforkf = new KeyfactorMetadataQuery(); - payloadforkf.Id = certificateid; - - if (digicertcertinstance["custom_fields"] != null) - // Getting custom metadata field values - foreach (var metadatafieldinstance in digicertcertinstance["custom_fields"]) - if (importallcustomdigicertfields) + // Now we process and prep the data for Keyfactor - first load manual fields. + var keyfactorMetadataPayload = new Dictionary(); + + + // Process manual fields + foreach (var field in unifiedFieldList.Where(f => f.ToolFieldType == UnifiedFieldType.Manual)) + try + { + var raw = Helpers.GetPropertyValue(dcResponse, field.DigicertFieldName)?.ToString(); + if (raw != null) { - // Using autoimport and thus using autorename - var metadatanamefield = ReplaceAllBannedCharacters(metadatafieldinstance["label"].ToString(), - allBannedChars); - payloadforkf.Metadata[metadatanamefield] = metadatafieldinstance["value"]; + using var doc = + JsonDocument.Parse( + $"\"{raw.Replace("\\", "\\\\").Replace("\"", "\\\"")}\""); // treat as a JSON string + var coerced = ValueCoercion.Coerce(doc.RootElement, + field.KeyfactorDataType, field.KeyfactorOptions); + if (coerced is not null && !(coerced is string s && string.IsNullOrWhiteSpace(s))) + keyfactorMetadataPayload[field.KeyfactorMetadataFieldName] = coerced; } - else + } + catch (Exception ex) + { + _logger.Warn( + $"[PAGE ERROR] Error processing manual field '{field.KeyfactorMetadataFieldName}' for cert {localKfCert.SerialNumber}: {ex.Message}"); + hasPartialProcessing = true; + } + + // Process custom fields + if (dcResponse.CustomFields != null && dcResponse.CustomFields.Count != 0) + foreach (var field in unifiedFieldList.Where(f => + f.ToolFieldType == UnifiedFieldType.Custom)) + try { - //Using custom names - var metadatanamequery = from customfieldinstance in kfcustomfields - where customfieldinstance.DigicertFieldName == - metadatafieldinstance["label"] - select customfieldinstance; - if (metadatanamequery.FirstOrDefault() != null) - payloadforkf.Metadata[metadatanamequery.FirstOrDefault().DigicertFieldName] = - metadatafieldinstance["value"]; + // Find the DC custom field by its DigiCert label + var localCustomField = dcResponse.CustomFields? + .FirstOrDefault(cf => cf.Label != null && + cf.Label.Equals(field.DigicertFieldName, + StringComparison.OrdinalIgnoreCase)); + + if (localCustomField != null) + { + var coerced = ValueCoercion.Coerce( + localCustomField.Value, + field.KeyfactorDataType, + field.KeyfactorOptions); + + // Only add non-null values; this avoids overwriting existing KF values with blanks. + if (coerced is not null && + !(coerced is string s && string.IsNullOrWhiteSpace(s))) + { + keyfactorMetadataPayload[field.KeyfactorMetadataFieldName] = coerced; + _logger.Trace($"Coerced DigiCert field '{field.DigicertFieldName}' to " + + $"Keyfactor '{field.KeyfactorMetadataFieldName}' as type {field.KeyfactorDataType}: {coerced}"); + } + else + { + _logger.Debug( + $"Skipping empty/null value for '{field.KeyfactorMetadataFieldName}' (source '{field.DigicertFieldName}')."); + } + } + } + catch (Exception ex) + { + _logger.Warn( + $"[PAGE ERROR] Error processing custom field '{field.KeyfactorMetadataFieldName}' for cert {localKfCert.SerialNumber}: {ex.Message}"); + hasPartialProcessing = true; + } + else + certsWithoutCustomFields++; + + // Update metadata in Keyfactor + if (keyfactorMetadataPayload.Count == 0) + _logger.Debug( + $"Skipping metadata upload for certificate {localKfCert.SerialNumber} as the payload is empty."); + else + try + { + if (kfClient.UpdateCertificateMetadata(localKfCert.Id, keyfactorMetadataPayload)) + { + cumulativeSuccessfullyUpdatedCerts.Add(localKfCert.SerialNumber); + _logger.Debug( + $"Updated metadata for certificate with Thumbprint {localKfCert.Thumbprint}"); } + } + catch (Exception ex) + { + _logger.Warn( + $"[PAGE ERROR] Error updating metadata for cert {localKfCert.SerialNumber}: {ex.Message}"); + hasPartialProcessing = true; + } - var flattenedcert = Flatten(digicertcertinstance); - //Getting manually selected metadata field values (not custom in DigiCert) - foreach (var manualinstance in kfmanualfields) - if (flattenedcert[manualinstance.DigicertFieldName] != null) - payloadforkf.Metadata[manualinstance.KeyfactorMetadataFieldName] = - flattenedcert[manualinstance.DigicertFieldName].ToString(); - //Sending the payload off to Keyfactor for the update - var finalsyncreq = new RestRequest(finalsyncurl); - finalsyncreq.AddHeader("Content-Type", "application/json"); - finalsyncreq.AddHeader("x-keyfactor-api-version", "1"); - finalsyncreq.AddHeader("x-keyfactor-requested-with", "APIClient"); - var serializedsyncfield = JsonConvert.SerializeObject(payloadforkf); - _logger.Trace("Sending Metadata update to KF for cert ID {0}, metadata update: {1}", - payloadforkf.Id.ToString(), serializedsyncfield); - - finalsyncreq.AddParameter("application/json", serializedsyncfield, ParameterType.RequestBody); - finalsyncclient.Put(finalsyncreq); - ++certcounttracker; + // Update counters + if (hasPartialProcessing) + cumulativePartiallyProcessedCerts.Add(localKfCert.SerialNumber); + else + totalCertsProcessed++; } + else if (configMode == ConfigMode.KFtoDC) + foreach (var localKfCert in certsPage) + if (localKfCert.Metadata != null && localKfCert.Metadata.Count != 0) + { + bool fullyUpdatedMetadata = true; + try + { + // --- call site patch --- + var dcResponse = + dcApiKeyClient.GetOrderBySerialOrThumbprint(localKfCert.SerialNumber, + localKfCert.Thumbprint); + var dcOrderId = dcResponse.Id; + + // Build a fast lookup for your mapping by KF metadata field name (case-insensitive) + var map = unifiedFieldList.Where(t => t.ToolFieldType == UnifiedFieldType.Custom) + .ToDictionary(f => f.KeyfactorMetadataFieldName, + f => f, + StringComparer.OrdinalIgnoreCase); + + foreach (var fieldInKf in localKfCert.Metadata) + { + if (!map.TryGetValue(fieldInKf.Key, out var u)) + continue; // no mapping for this KF field - + var dcMetadataId = + u.DigiCertMetadaFieldId; // ensure your model uses this exact name + if (dcMetadataId <= 0) + continue; // can't push without DigiCert metadata_id + // Pick DigiCert type: config wins; else derive from KF type + var dcType = u.DigicertDataType != DigiCertCustomFieldDataType.Anything + ? u.DigicertDataType + : Helpers.ToDigiCertDataType(u.KeyfactorDataType); - // Update the count of items downloaded so far - certsdownloaded += batchsize; + var raw = fieldInKf.Value; // often string; can be object - normalize: + var rawString = raw; - // Check if all items have been downloaded - if (certlist.Count == 0) - { - Console.WriteLine( - $"Metadata sync from Keyfactor to DigiCert complete. Number of certs synced: {certcounttracker.ToString()}"); - _logger.Debug( - $"Metadata sync from Keyfactor to DigiCert complete. Number of certs synced: {certcounttracker.ToString()}"); + var coerced = + ValueCoercionDC.CoerceForDigiCert(rawString, dcType, u.KeyfactorOptions); - break; - } + if (!dcApiKeyClient.UpdateOrderCustomFieldValue(dcOrderId, dcMetadataId, + coerced)) + { + fullyUpdatedMetadata = false; + _logger.Warn( + $"Failed to update DigiCert custom field '{u.DigicertFieldName}' for cert {localKfCert.SerialNumber}"); + } + } + + if (fullyUpdatedMetadata) + { + totalCertsProcessed++; // Increment total processed count + cumulativeSuccessfullyUpdatedCerts.Add(localKfCert.SerialNumber); + } + else + { + cumulativePartiallyProcessedCerts.Add(localKfCert.SerialNumber); + } + } + catch (Exception ex) + { + _logger.Error( + $"Error updating DigiCert custom field for cert {localKfCert.SerialNumber}: {ex.Message}"); + } + } + else + { + certsWithoutCustomFields++; + cumulativeMissingCustomFields.Add(localKfCert.SerialNumber); + } + else + throw new ArgumentException("Invalid configuration mode. Please specify KFtoSC or SCtoKF."); + // Flushing lists to avoid memory issues on large syncs + cumulativeSuccessfullyUpdatedCerts.FlushRemainder( + _logger, + label: "SuccessfullyUpdated", + totalCount: ref successfullyUpdatedCount + ); + cumulativePartiallyProcessedCerts.FlushRemainder( + _logger, + label: "PartiallyProcessed", + totalCount: ref partiallyProcessedCount + ); + cumulativeUnmatchedCerts.FlushRemainder( + _logger, + label: "UnmatchedBetweenKfAndDc", + totalCount: ref unmatchedCount + ); + cumulativeMissingCustomFields.FlushRemainder( + _logger, + label: "MissingCustomFields", + totalCount: ref missingCustomFields + ); + } + else + { + hasMorePages = false; // No more certificates to retrieve } } - Environment.Exit(0); + // Log cumulative results before the application finishes + _logger.Info( + $"[SUMMARY] Completed retrieval and processing of certificates. Total certificates processed successfully: {totalCertsProcessed}. Certs without Custom Fields data: {certsWithoutCustomFields}."); + if (partiallyProcessedCount + unmatchedCount > 0) + _logger.Warn( + $"[SUMMARY] Total certificates with partial processing or errors: {partiallyProcessedCount + unmatchedCount}."); + if (unmatchedCount > 0) + _logger.Warn( + $"[SUMMARY] No matching DigiCert certificates found for {unmatchedCount} Keyfactor certs."); + // Log aggregated warnings for missing custom fields during SCtoKF sync + if (missingCustomFields > 0) + { + _logger.Info( + $"[SUMMARY] No Metadata found for {missingCustomFields} DigiCert certificates in Keyfactor."); + } + // End of the run + _logger.Info("============================================================"); + _logger.Info($"[END] DigiCert Metadata Sync - Run completed at {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); + _logger.Info($"[RUN ID: {runId}]"); + _logger.Info("============================================================"); } -} +} \ No newline at end of file diff --git a/digicert-metadata-sync/Models/CharDBItem.cs b/digicert-metadata-sync/Models/CharDBItem.cs deleted file mode 100644 index 63e12c9..0000000 --- a/digicert-metadata-sync/Models/CharDBItem.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2021 Keyfactor -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace DigicertMetadataSync; - -partial class DigicertSync -{ - - public class CharDBItem - { - public string character; - public string replacementcharacter; - } - -} \ No newline at end of file diff --git a/digicert-metadata-sync/Models/Config.cs b/digicert-metadata-sync/Models/Config.cs new file mode 100644 index 0000000..2279385 --- /dev/null +++ b/digicert-metadata-sync/Models/Config.cs @@ -0,0 +1,28 @@ +// Copyright 2024 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. +namespace DigicertMetadataSync.Models; + +public class Config +{ + public string digicertApiKey { get; set; } = ""; + public string keyfactorDomainAndUser { get; set; } = ""; + public string keyfactorPassword { get; set; } = ""; + public string keyfactorAPIUrl { get; set; } = ""; + public string keyfactorDigicertIssuedCertQueryTerm { get; set; } = "DigiCert"; + public bool importAllCustomDigicertFields { get; set; } = false; + public bool importDataForDeactivatedDigiCertFields { get; set; } = false; + public bool syncRevokedAndExpiredCerts { get; set; } = false; + public int keyfactorPageSize { get; set; } = 100; + public string keyfactorDateFormat { get; set; } + public bool createMissingFieldsInDigicert { get; set; } = false; +} + +public enum ConfigMode +{ + KFtoDC, // Keyfactor to Sectigo + DCtoKF // Sectigo to Keyfactor +} diff --git a/digicert-metadata-sync/Models/DigicertCertInstance.cs b/digicert-metadata-sync/Models/DigicertCertInstance.cs deleted file mode 100644 index e96dc9d..0000000 --- a/digicert-metadata-sync/Models/DigicertCertInstance.cs +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright 2021 Keyfactor -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace DigicertMetadataSync; - -internal partial class DigicertSync -{ - public class DigicertCert - { - public int id { get; set; } - public DigicertCertificate certificate { get; set; } - public string status { get; set; } - public bool is_renewal { get; set; } - public DateTime date_created { get; set; } - public DigicertOrganization organization { get; set; } - public int validity_years { get; set; } - public bool disable_renewal_notifications { get; set; } - public int auto_renew { get; set; } - public int auto_reissue { get; set; } - public DigicertContainer container { get; set; } - public DigicertProduct product { get; set; } - public DigicertOrganization_Contact organization_contact { get; set; } - public DigicertTechnical_Contact technical_contact { get; set; } - public DigicertUser user { get; set; } - public int purchased_dns_names { get; set; } - public DigicertRequest[] requests { get; set; } - public bool is_out_of_contract { get; set; } - public string payment_method { get; set; } - public string product_name_id { get; set; } - public DigicertCustom_Fields[] custom_fields { get; set; } - public bool disable_issuance_email { get; set; } - public bool is_guest_access_enabled { get; set; } - } - - public class DigicertCertificate - { - public string common_name { get; set; } - public string[] dns_names { get; set; } - public DateTime date_created { get; set; } - public string csr { get; set; } - public string serial_number { get; set; } - public DigicertCertOrganization organization { get; set; } - public string[] organization_units { get; set; } - public DigicertServer_Platform server_platform { get; set; } - public string signature_hash { get; set; } - public int key_size { get; set; } - public DigicertCa_Cert ca_cert { get; set; } - } - - public class DigicertCertOrganization - { - public int id { get; set; } - } - - public class DigicertServer_Platform - { - public int id { get; set; } - public string name { get; set; } - public string install_url { get; set; } - public string csr_url { get; set; } - } - - public class DigicertCa_Cert - { - public string id { get; set; } - public string name { get; set; } - } - - public class DigicertOrganization - { - public int id { get; set; } - public string name { get; set; } - public string assumed_name { get; set; } - public string display_name { get; set; } - public string city { get; set; } - public string state { get; set; } - public string country { get; set; } - } - - public class DigicertContainer - { - public int id { get; set; } - public string name { get; set; } - public bool is_active { get; set; } - } - - public class DigicertProduct - { - public string name_id { get; set; } - public string name { get; set; } - public string type { get; set; } - public string validation_type { get; set; } - public string validation_name { get; set; } - public string validation_description { get; set; } - public bool csr_required { get; set; } - } - - public class DigicertOrganization_Contact - { - public string first_name { get; set; } - public string last_name { get; set; } - public string email { get; set; } - public string job_title { get; set; } - public string telephone { get; set; } - public string telephone_extension { get; set; } - } - - public class DigicertTechnical_Contact - { - public string first_name { get; set; } - public string last_name { get; set; } - public string email { get; set; } - public string job_title { get; set; } - public string telephone { get; set; } - public string telephone_extension { get; set; } - } - - public class DigicertUser - { - public int id { get; set; } - public string first_name { get; set; } - public string last_name { get; set; } - public string email { get; set; } - } - - public class DigicertRequest - { - public int id { get; set; } - public DateTime date { get; set; } - public string type { get; set; } - public string status { get; set; } - public string comments { get; set; } - } - - public class DigicertCustom_Fields - { - public int metadata_id { get; set; } - public string label { get; set; } - public string value { get; set; } - } -} \ No newline at end of file diff --git a/digicert-metadata-sync/Models/DigicertCertificate.cs b/digicert-metadata-sync/Models/DigicertCertificate.cs new file mode 100644 index 0000000..b8c847b --- /dev/null +++ b/digicert-metadata-sync/Models/DigicertCertificate.cs @@ -0,0 +1,99 @@ +// Copyright 2024 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. +using System.Text.Json.Serialization; + +namespace DigicertMetadataSync.Models; + +public sealed class DcOrderCertificate +{ + // Core IDs & identifiers + [JsonPropertyName("id")] public int? Id { get; set; } + [JsonPropertyName("thumbprint")] public string? Thumbprint { get; set; } + [JsonPropertyName("serial_number")] public string? SerialNumber { get; set; } + [JsonPropertyName("common_name")] public string? CommonName { get; set; } + + // Subject alternative names / S/MIME + [JsonPropertyName("dns_names")] public List? DnsNames { get; set; } + [JsonPropertyName("emails")] public List? Emails { get; set; } // S/MIME only + + // Timestamps (API uses ISO 8601 for date_created/issued; yyyy-MM-dd for valid_*). + // Keep strings to avoid DateOnly converters; parse upstream if you prefer DateOnly/DateTimeOffset. + [JsonPropertyName("date_created")] public string? DateCreated { get; set; } + [JsonPropertyName("date_issued")] public string? DateIssued { get; set; } + [JsonPropertyName("valid_from")] public string? ValidFrom { get; set; } // "YYYY-MM-DD" + [JsonPropertyName("valid_till")] public string? ValidTill { get; set; } // "YYYY-MM-DD" + + [JsonPropertyName("days_remaining")] public int? DaysRemaining { get; set; } + + // CSR (not returned for VMC) + [JsonPropertyName("csr")] public string? Csr { get; set; } + + // Organization (on the certificate) & OUs + [JsonPropertyName("organization")] public DcCertificateOrganizationRef? Organization { get; set; } + + [JsonPropertyName("organization_units")] + public List? OrganizationUnits { get; set; } + + // Server platform info (TLS) + [JsonPropertyName("server_platform")] public DcServerPlatform? ServerPlatform { get; set; } + + // Crypto characteristics + [JsonPropertyName("signature_hash")] public string? SignatureHash { get; set; } // e.g., "sha256" + [JsonPropertyName("key_size")] public int? KeySize { get; set; } + + // Issuing CA information + [JsonPropertyName("ca_cert")] public DcCaCertRef? CaCert { get; set; } + + // Validity overrides for the *certificate* (varies by product) + [JsonPropertyName("cert_validity")] public DcCertValidity? CertValidity { get; set; } + + // Fields that appear in DigiCert’s examples for some products + [JsonPropertyName("user_id")] public int? UserId { get; set; } + + // DigiCert examples sometimes serialize counts as strings; allow numbers-in-strings. + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + [JsonPropertyName("purchased_dns_names")] + public int? PurchasedDnsNames { get; set; } + + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + [JsonPropertyName("purchased_wildcard_names")] + public int? PurchasedWildcardNames { get; set; } + + // Receipt ID appears as string in examples + [JsonPropertyName("receipt_id")] public string? ReceiptId { get; set; } +} + +public sealed class DcCertValidity +{ + [JsonPropertyName("years")] public int? Years { get; set; } + [JsonPropertyName("days")] public int? Days { get; set; } + + // DigiCert documents custom expiration as a string in requests (e.g., "09 JUN 2025"); + // responses may include it when used. Keep it as string for maximum compatibility. + [JsonPropertyName("custom_expiration_date")] + public string? CustomExpirationDate { get; set; } +} + +public sealed class DcCertificateOrganizationRef +{ + [JsonPropertyName("id")] public int? Id { get; set; } +} + +public sealed class DcServerPlatform +{ + [JsonPropertyName("id")] public int? Id { get; set; } + [JsonPropertyName("name")] public string? Name { get; set; } + [JsonPropertyName("install_url")] public string? InstallUrl { get; set; } + [JsonPropertyName("csr_url")] public string? CsrUrl { get; set; } +} + +public sealed class DcCaCertRef +{ + // DigiCert shows this as a string token (e.g., "DF3689F672CCB90C") + [JsonPropertyName("id")] public string? Id { get; set; } + [JsonPropertyName("name")] public string? Name { get; set; } +} \ No newline at end of file diff --git a/digicert-metadata-sync/Models/DigicertCustomField.cs b/digicert-metadata-sync/Models/DigicertCustomField.cs deleted file mode 100644 index 3e2def7..0000000 --- a/digicert-metadata-sync/Models/DigicertCustomField.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2021 Keyfactor -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace DigicertMetadataSync; - -internal partial class DigicertSync -{ - public class DigicertCustomFieldInstance - { - public int id { get; set; } = 999999999; - public string label { get; set; } = ""; - public bool is_required { get; set; } = false; - public bool is_active { get; set; } = true; - public string data_type { get; set; } = "anything"; - public string kf_field_name { get; set; } = ""; - } - - public class DigicertMetadataUpdateInstance - { - public int metadata_id { get; set; } = 999999999; - public string value { get; set; } = "false"; - } -} \ No newline at end of file diff --git a/digicert-metadata-sync/Models/DigicertLookup.cs b/digicert-metadata-sync/Models/DigicertLookup.cs deleted file mode 100644 index 365c43e..0000000 --- a/digicert-metadata-sync/Models/DigicertLookup.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2021 Keyfactor -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace DigicertMetadataSync; - -internal partial class DigicertSync -{ - public class RootDigicertLookup - { - public List searchCriteriaList = new(); - - public string accountId { get; set; } = "x"; - } - - public class SearchCriterion - { - public List value = new(); - public string key { get; set; } = "serialNum"; - public string operation { get; set; } = "EQUALS"; - } -} \ No newline at end of file diff --git a/digicert-metadata-sync/Models/DigicertModels.cs b/digicert-metadata-sync/Models/DigicertModels.cs new file mode 100644 index 0000000..8059b0a --- /dev/null +++ b/digicert-metadata-sync/Models/DigicertModels.cs @@ -0,0 +1,468 @@ +// Copyright 2024 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. +using System.Text.Json; +using System.Text.Json.Serialization; +using static DigicertMetadataSync.Logic.Helpers; + +namespace DigicertMetadataSync.Models; + +// ---- Account/metadata ---- +public sealed class DcCustomFieldListRoot +{ + [JsonPropertyName("metadata")] public List? Metadata { get; set; } +} + +public sealed class DcCustomField +{ + [JsonPropertyName("id")] public int Id { get; set; } + [JsonPropertyName("label")] public string? Label { get; set; } + [JsonPropertyName("is_required")] public bool? IsRequired { get; set; } + [JsonPropertyName("is_active")] public bool? IsActive { get; set; } + [JsonPropertyName("data_type")] public string? DataType { get; set; } = "_"; // text, int, email_address, email_list, etc. + [JsonPropertyName("show_in_receipt")] public bool? ShowInReceipt { get; set; } +} + +public sealed class DcCustomFieldCreate +{ + [JsonPropertyName("label")] public string Label { get; set; } = string.Empty; + + [JsonPropertyName("is_required")] public bool? IsRequired { get; set; } + + [JsonPropertyName("is_active")] public bool? IsActive { get; set; } + + // Enum for internal use + [JsonIgnore] public DigiCertCustomFieldDataType DataTypeEnum { get; set; } = DigiCertCustomFieldDataType.Anything; + + // Wire property (DigiCert expects string or omitted) + [JsonPropertyName("data_type")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? DataType + { + get => DcCustomFieldTypeMapper.ToWireString(DataTypeEnum); + set + { + // optional tolerant parsing if you ever read it back + DataTypeEnum = value?.ToLowerInvariant() switch + { + "text" => DigiCertCustomFieldDataType.Text, + "int" => DigiCertCustomFieldDataType.Int, + "email_address" => DigiCertCustomFieldDataType.EmailAddress, + "email_list" => DigiCertCustomFieldDataType.EmailList, + _ => DigiCertCustomFieldDataType.Anything + }; + } + } + + [JsonPropertyName("show_in_receipt")] public bool? ShowInReceipt { get; set; } = true; +} + +public sealed class DcCustomFieldValueUpdate +{ + [JsonPropertyName("metadata_id")] public int MetadataId { get; set; } + [JsonPropertyName("value")] public string Value { get; set; } = string.Empty; +} + +public sealed class DcCustomFieldUpdate +{ + [JsonPropertyName("label")] public string? Label { get; set; } + [JsonPropertyName("is_required")] public bool? IsRequired { get; set; } + [JsonPropertyName("is_active")] public bool? IsActive { get; set; } + [JsonPropertyName("data_type")] public string? DataType { get; set; } + [JsonPropertyName("show_in_receipt")] public bool? ShowInReceipt { get; set; } +} + +// ---- Orders ---- +public sealed class DcOrderList +{ + [JsonPropertyName("orders")] public List? Orders { get; set; } + [JsonPropertyName("page")] public DcPage? Page { get; set; } +} + +public sealed class DcPage +{ + [JsonPropertyName("total")] public int? Total { get; set; } + [JsonPropertyName("limit")] public int? Limit { get; set; } + [JsonPropertyName("offset")] public int? Offset { get; set; } +} + +public sealed class DcOrderListItem +{ + [JsonPropertyName("id")] public int Id { get; set; } + [JsonPropertyName("certificate")] public DcOrderCertificateSummary? Certificate { get; set; } + [JsonPropertyName("status")] public string? Status { get; set; } + [JsonPropertyName("date_created")] public string? DateCreated { get; set; } + [JsonPropertyName("product_name_id")] public string? ProductNameId { get; set; } + [JsonPropertyName("has_duplicates")] public bool? HasDuplicates { get; set; } + [JsonPropertyName("duplicates_count")] public int? DuplicatesCount { get; set; } + [JsonPropertyName("reissues_count")] public int? ReissuesCount { get; set; } +} + +public sealed class DcOrderCertificateSummary +{ + [JsonPropertyName("id")] public int? Id { get; set; } + [JsonPropertyName("common_name")] public string? CommonName { get; set; } + [JsonPropertyName("serial_number")] public string? SerialNumber { get; set; } + [JsonPropertyName("thumbprint")] public string? Thumbprint { get; set; } +} + +public sealed class DcOrderInfo +{ + // Core + [JsonPropertyName("id")] public int Id { get; set; } + [JsonPropertyName("status")] public string? Status { get; set; } + + // Existing model (your class) + [JsonPropertyName("certificate")] public DcOrderCertificate? Certificate { get; set; } + + // Contacts / org / container + [JsonPropertyName("organization")] public DcOrderOrganization? Organization { get; set; } + + [JsonPropertyName("organization_contact")] + public DcOrderContact? OrganizationContact { get; set; } + + [JsonPropertyName("technical_contact")] + public DcOrderContact? TechnicalContact { get; set; } + + [JsonPropertyName("verified_contacts")] + public List? VerifiedContacts { get; set; } // EV & others + + [JsonPropertyName("container")] public DcOrderContainer? Container { get; set; } + + // Product block + [JsonPropertyName("product")] public DcOrderProduct? Product { get; set; } + [JsonPropertyName("product_name_id")] public string? ProductNameId { get; set; } // convenience alias + + // Emails & custom fields + [JsonPropertyName("additional_emails")] + public List? AdditionalEmails { get; set; } + + [JsonPropertyName("custom_fields")] public List? CustomFields { get; set; } + + // Pricing & payment + [JsonPropertyName("price")] public decimal? Price { get; set; } + [JsonPropertyName("currency")] public string? Currency { get; set; } + [JsonPropertyName("prices")] public List? Prices { get; set; } // reissue totals by currency + [JsonPropertyName("payment_method")] public string? PaymentMethod { get; set; } // balance|card|subscription + [JsonPropertyName("payment_profile")] public DcPaymentProfile? PaymentProfile { get; set; } + + [JsonPropertyName("is_out_of_contract")] + public bool? IsOutOfContract { get; set; } + + // Order features / toggles + [JsonPropertyName("disable_issuance_email")] + public bool? DisableIssuanceEmail { get; set; } + + [JsonPropertyName("disable_ct")] public bool? DisableCt { get; set; } + [JsonPropertyName("allow_duplicates")] public bool? AllowDuplicates { get; set; } + [JsonPropertyName("duplicates_count")] public int? DuplicatesCount { get; set; } + [JsonPropertyName("reissues_count")] public int? ReissuesCount { get; set; } + [JsonPropertyName("server_licenses")] public int? ServerLicenses { get; set; } + + [JsonPropertyName("is_guest_access_enabled")] + public bool? IsGuestAccessEnabled { get; set; } + + [JsonPropertyName("has_pending_request")] + public bool? HasPendingRequest { get; set; } + + // Renewals / multi-year plan + [JsonPropertyName("is_renewal")] public bool? IsRenewal { get; set; } + [JsonPropertyName("renewed_order_id")] public int? RenewedOrderId { get; set; } + + [JsonPropertyName("renewal_of_order_id")] + public int? RenewalOfOrderId { get; set; } + + // DigiCert returns "1" when true (omitted otherwise). Keep as string to match raw. + [JsonPropertyName("is_multi_year_plan")] + public string? IsMultiYearPlan { get; set; } + + // Approvals & assignments + [JsonPropertyName("order_approval_complete")] + public bool? OrderApprovalComplete { get; set; } // EV TLS/SSL + + [JsonPropertyName("approver")] public DcUserDetails? Approver { get; set; } // the user who approved + [JsonPropertyName("user_assignments")] public List? UserAssignments { get; set; } + + // API key provenance + [JsonPropertyName("api_key")] public DcOrderApiKey? ApiKey { get; set; } + + // Competitive replacement & benefits + [JsonPropertyName("benefits")] public DcOrderBenefits? Benefits { get; set; } + + // Alternative identifiers + [JsonPropertyName("alternative_order_id")] + public string? AlternativeOrderId { get; set; } + + // VMC / CMC (only for those products) + [JsonPropertyName("vmc")] public DcOrderVmc? Vmc { get; set; } + + // Verification rollup (statuses vary by product — strings such as pending/complete) + [JsonPropertyName("verification")] public DcOrderVerification? Verification { get; set; } + + // Requests history summary (reissues/duplicates/etc.) — schema varies; keep minimal + [JsonPropertyName("requests")] public List? Requests { get; set; } + + // Future-proofing: capture anything DigiCert adds later + [JsonExtensionData] public Dictionary? ExtensionData { get; set; } +} + +// -------- nested models -------- + +public sealed class DcOrderProduct +{ + [JsonPropertyName("name_id")] public string? NameId { get; set; } // e.g., ssl_basic, code_signing_ev, vmc_basic + [JsonPropertyName("type_hint")] public string? TypeHint { get; set; } // sometimes present + [JsonPropertyName("type_id")] public int? TypeId { get; set; } + [JsonPropertyName("brand")] public string? Brand { get; set; } + [JsonPropertyName("description")] public string? Description { get; set; } + [JsonExtensionData] public Dictionary? Ext { get; set; } +} + +public sealed class DcOrderContainer +{ + [JsonPropertyName("id")] public int? Id { get; set; } + [JsonPropertyName("name")] public string? Name { get; set; } + [JsonExtensionData] public Dictionary? Ext { get; set; } +} + +public sealed class DcOrderOrganization +{ + [JsonPropertyName("id")] public int? Id { get; set; } + [JsonPropertyName("name")] public string? Name { get; set; } + [JsonPropertyName("assumed_name")] public string? AssumedName { get; set; } // DBA + [JsonPropertyName("display_name")] public string? DisplayName { get; set; } + [JsonPropertyName("address")] public DcOrgAddress? Address { get; set; } + [JsonPropertyName("telephone")] public string? Telephone { get; set; } + [JsonExtensionData] public Dictionary? Ext { get; set; } +} + +public sealed class DcOrgAddress +{ + [JsonPropertyName("street_address")] public string? StreetAddress { get; set; } + [JsonPropertyName("locality")] public string? Locality { get; set; } + [JsonPropertyName("state")] public string? State { get; set; } + [JsonPropertyName("postal_code")] public string? PostalCode { get; set; } + [JsonPropertyName("country")] public string? Country { get; set; } +} + +public sealed class DcOrderContact +{ + [JsonPropertyName("id")] public int? Id { get; set; } + [JsonPropertyName("name")] public string? Name { get; set; } + [JsonPropertyName("first_name")] public string? FirstName { get; set; } + [JsonPropertyName("last_name")] public string? LastName { get; set; } + [JsonPropertyName("email")] public string? Email { get; set; } + [JsonPropertyName("telephone")] public string? Telephone { get; set; } + [JsonPropertyName("job_title")] public string? JobTitle { get; set; } + [JsonExtensionData] public Dictionary? Ext { get; set; } +} + +public sealed class DcUserDetails +{ + [JsonPropertyName("id")] public int? Id { get; set; } + [JsonPropertyName("first_name")] public string? FirstName { get; set; } + [JsonPropertyName("last_name")] public string? LastName { get; set; } + [JsonPropertyName("email")] public string? Email { get; set; } + [JsonExtensionData] public Dictionary? Ext { get; set; } +} + +public sealed class DcOrderApiKey +{ + [JsonPropertyName("name")] public string? Name { get; set; } // API key name or ACME Directory URL + [JsonPropertyName("key_type")] public string? KeyType { get; set; } // api_key | acme_url +} + +public sealed class DcOrderBenefits +{ + [JsonPropertyName("actual_price")] public string? ActualPrice { get; set; } + [JsonPropertyName("discount_percent")] public string? DiscountPercent { get; set; } + [JsonPropertyName("benefits")] public List? BenefitTypes { get; set; } // e.g., cr_benefit + [JsonPropertyName("benefits_data")] public DcOrderBenefitsData? BenefitsData { get; set; } +} + +public sealed class DcOrderBenefitsData +{ + [JsonPropertyName("cr_benefit")] public DcCompetitiveReplacement? CompetitiveReplacement { get; set; } +} + +public sealed class DcCompetitiveReplacement +{ + [JsonPropertyName("type")] public string? Type { get; set; } // DISCOUNT + + [JsonPropertyName("discount_percentage")] + public float? DiscountPercentage { get; set; } + + [JsonPropertyName("premium_discount_percentage")] + public float? PremiumDiscountPercentage { get; set; } + + [JsonPropertyName("availed_domains")] public List? AvailedDomains { get; set; } +} + +public sealed class DcOrderPrice +{ + [JsonPropertyName("price")] public decimal? Value { get; set; } + [JsonPropertyName("currency")] public string? Currency { get; set; } +} + +public sealed class DcPaymentProfile +{ + [JsonPropertyName("id")] public int? Id { get; set; } + [JsonPropertyName("name")] public string? Name { get; set; } + [JsonPropertyName("card_type")] public string? CardType { get; set; } + [JsonPropertyName("masked_number")] public string? MaskedNumber { get; set; } + [JsonPropertyName("exp_month")] public int? ExpMonth { get; set; } + [JsonPropertyName("exp_year")] public int? ExpYear { get; set; } + [JsonExtensionData] public Dictionary? Ext { get; set; } +} + +public sealed class DcOrderVmc +{ + [JsonPropertyName("logo_id")] public int? LogoId { get; set; } + [JsonPropertyName("logo")] public string? LogoBase64 { get; set; } + [JsonPropertyName("enable_hosting")] public bool? EnableHosting { get; set; } + + [JsonPropertyName("hosted_logo_location")] + public string? HostedLogoLocation { get; set; } + + [JsonPropertyName("hosted_cert_location")] + public string? HostedCertLocation { get; set; } + + [JsonPropertyName("mark_type")] public string? MarkType { get; set; } // registered_mark | government_mark + [JsonPropertyName("mark_type_data")] public DcVmcMarkTypeData? MarkTypeData { get; set; } +} + +public sealed class DcVmcMarkTypeData +{ + [JsonPropertyName("country_code")] public string? CountryCode { get; set; } + [JsonPropertyName("state_province")] public string? StateProvince { get; set; } + [JsonPropertyName("locality")] public string? Locality { get; set; } +} + +// Verification rollup (statuses are strings like "pending", "complete"; presence varies by product) +public sealed class DcOrderVerification +{ + [JsonPropertyName("organization_type")] + public string? OrganizationType { get; set; } + + [JsonPropertyName("organization_status")] + public string? OrganizationStatus { get; set; } + + [JsonPropertyName("address_verification")] + public string? AddressVerification { get; set; } + + [JsonPropertyName("blacklist_fraud")] public string? BlacklistFraud { get; set; } + + [JsonPropertyName("blacklist_fraud_malware")] + public string? BlacklistFraudMalware { get; set; } + + [JsonPropertyName("request_authenticity")] + public string? RequestAuthenticity { get; set; } + + [JsonPropertyName("operational_existence")] + public string? OperationalExistence { get; set; } + + [JsonPropertyName("place_of_business_verification")] + public string? PlaceOfBusinessVerification { get; set; } + + [JsonPropertyName("phone_number_verification")] + public string? PhoneNumberVerification { get; set; } + + [JsonPropertyName("approver_verification")] + public string? ApproverVerification { get; set; } + + [JsonExtensionData] public Dictionary? Ext { get; set; } +} + +public sealed class DcOrderRequestSummary +{ + [JsonPropertyName("id")] public int? Id { get; set; } + [JsonPropertyName("type")] public string? Type { get; set; } // duplicate | reissue | renewal | etc. + [JsonPropertyName("status")] public string? Status { get; set; } // pending | complete | etc. + [JsonPropertyName("date_created")] public string? DateCreated { get; set; } // ISO-8601 + [JsonExtensionData] public Dictionary? Ext { get; set; } +} + +public sealed class DcOrderCustomField +{ + [JsonPropertyName("id")] public int? Id { get; set; } + [JsonPropertyName("metadata_id")] public int? MetadataId { get; set; } + [JsonPropertyName("label")] public string? Label { get; set; } + [JsonPropertyName("value")] public JsonElement Value { get; set; } +} + +public sealed class DcOrderCustomFieldValue +{ + [JsonPropertyName("metadata_id")] public int MetadataId { get; set; } + [JsonPropertyName("value")] public string Value { get; set; } = string.Empty; +} +// ---------------------- Minimal helper types you can add ---------------------- + +public sealed class ReportsQueryRequest +{ + [JsonPropertyName("query")] public string Query { get; set; } = string.Empty; + [JsonPropertyName("variables")] public ReportsQueryVars Variables { get; set; } = new(); +} + +public sealed class ReportsQueryVars +{ + // `t` matches $t in the GraphQL query + [JsonPropertyName("t")] public string T { get; set; } = string.Empty; +} + +public sealed class ReportsQueryResponse +{ + [JsonPropertyName("data")] public TData? Data { get; set; } + [JsonPropertyName("errors")] public object? Errors { get; set; } // ignored but useful to log if needed +} + +public sealed class ReportsOrderDetailsData +{ + [JsonPropertyName("order_details")] public List? OrderDetails { get; set; } +} + +public sealed class ReportsOrderId +{ + [JsonPropertyName("id")] public string? Id { get; set; } +} + +public enum DigiCertCustomFieldDataType +{ + Anything = 0, + Text = 1, + Int = 2, + EmailAddress = 3, + EmailList = 4 +} + +public static class DigiCertFieldTypeWire +{ + // enum -> wire string (null = omit field) + public static string? ToWireString(this DigiCertCustomFieldDataType t) + { + return t switch + { + DigiCertCustomFieldDataType.Anything => null, // omit per DigiCert spec + DigiCertCustomFieldDataType.Text => "text", + DigiCertCustomFieldDataType.Int => "int", + DigiCertCustomFieldDataType.EmailAddress => "email_address", + DigiCertCustomFieldDataType.EmailList => "email_list", + _ => null + }; + } + + // optional: wire string -> enum + public static DigiCertCustomFieldDataType Parse(string? s) + { + return (s ?? "").Trim().ToLowerInvariant() switch + { + "text" => DigiCertCustomFieldDataType.Text, + "int" => DigiCertCustomFieldDataType.Int, + "email_address" => DigiCertCustomFieldDataType.EmailAddress, + "email_list" => DigiCertCustomFieldDataType.EmailList, + _ => DigiCertCustomFieldDataType.Anything + }; + } +} \ No newline at end of file diff --git a/digicert-metadata-sync/Models/Internal.cs b/digicert-metadata-sync/Models/Internal.cs new file mode 100644 index 0000000..e3b7847 --- /dev/null +++ b/digicert-metadata-sync/Models/Internal.cs @@ -0,0 +1,74 @@ +// Copyright 2024 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. +using System.Text.Json.Serialization; +using Microsoft.Extensions.Configuration; + +namespace DigicertMetadataSync.Models; + +/// +/// Represents a unified format for metadata fields. +/// +public class UnifiedFormatField +{ + private DigiCertCustomFieldDataType _dcType = DigiCertCustomFieldDataType.Anything; + private KeyfactorMetadataDataType _kfType = KeyfactorMetadataDataType.String; + public int DigiCertMetadaFieldId = 0; // Default to 0 (not set) + public string DigicertFieldName { get; set; } = string.Empty; + public string KeyfactorMetadataFieldName { get; set; } = string.Empty; + + public string KeyfactorDescription { get; set; } = string.Empty; + + // Bind integer codes from fields.json + // "keyfactorDataType": 2 -> KeyfactorMetadataDataType.Integer + // ---- enum properties you should use everywhere in code ---- + [ConfigurationKeyName("keyfactorDataType")] + [JsonPropertyName("keyfactorDataType")] + public KeyfactorMetadataDataType KeyfactorDataType + { + get => _kfType; + set => _kfType = Enum.IsDefined(typeof(KeyfactorMetadataDataType), value) + ? value + : KeyfactorMetadataDataType.String; // safe default when 0/unknown comes in + } + + [ConfigurationKeyName("digicertCustomFieldDataType")] + [JsonPropertyName("digicertCustomFieldDataType")] + public DigiCertCustomFieldDataType DigicertDataType + { + get => _dcType; + set => _dcType = Enum.IsDefined(typeof(DigiCertCustomFieldDataType), value) + ? value + : DigiCertCustomFieldDataType.Anything; // omit on wire by default + } + + public string? KeyfactorHint { get; set; } + public string? KeyfactorValidation { get; set; } + public int KeyfactorEnrollment { get; set; } // Default to Optional + public string? KeyfactorMessage { get; set; } + public string[]? KeyfactorOptions { get; set; } // Added for options mapping + public string? KeyfactorDefaultValue { get; set; } + public int KeyfactorDisplayOrder { get; set; } + public bool KeyfactorCaseSensitive { get; set; } = false; // Default to false + public int KeyfactorMetadataFieldId { get; set; } // Default to 0 (not set) + public bool KeyfactorAllowAPI { get; set; } = true; + public UnifiedFieldType ToolFieldType { get; set; } = UnifiedFieldType.Custom; // Default to Custom +} + +/// +/// Used for storage of replacement characters during field conversion. +/// +public class CharDBItem +{ + public string character { get; set; } = string.Empty; + public string replacementcharacter { get; set; } = string.Empty; +} + +public enum UnifiedFieldType +{ + Manual = 1, + Custom = 2 +} \ No newline at end of file diff --git a/digicert-metadata-sync/Models/InternalClasses.cs b/digicert-metadata-sync/Models/InternalClasses.cs deleted file mode 100644 index 5c0d3eb..0000000 --- a/digicert-metadata-sync/Models/InternalClasses.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2021 Keyfactor -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace DigicertMetadataSync; - -internal partial class DigicertSync -{ - public class CustomDigicertMetadataInstance - { - public int id { get; set; } - public string label { get; set; } - public bool is_required { get; set; } - public bool is_active { get; set; } - public string description { get; set; } - public string data_type { get; set; } - } - - public class ReadInMetadataField - { - public string DigicertFieldName { get; set; } = "local_test_nullx0"; - public string KeyfactorMetadataFieldName { get; set; } = "test_name_nullx0"; - public string KeyfactorDescription { get; set; } = "None."; - public string KeyfactorDataType { get; set; } = "string"; - public string KeyfactorHint { get; set; } = "None."; - public string KeyfactorAllowAPI { get; set; } = "True"; - public string FieldType { get; set; } = "manual/custom"; - } - - public class KeyfactorMetadataInstanceSendoff - { - public string Name { get; set; } = ""; - - public string Description { get; set; } = "No description provided."; - - //Default field type is set to 1 for Keyfactor - string - public int DataType { get; set; } = 1; - public string Hint { get; set; } = ""; - public bool AllowAPI { get; set; } = true; - } -} \ No newline at end of file diff --git a/digicert-metadata-sync/Models/KeyfactorCertInstance.cs b/digicert-metadata-sync/Models/KeyfactorCertInstance.cs deleted file mode 100644 index 4188851..0000000 --- a/digicert-metadata-sync/Models/KeyfactorCertInstance.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2021 Keyfactor -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace DigicertMetadataSync; - -internal partial class DigicertSync -{ - public class KeyfactorCert - { - public int Id { get; set; } - public string Thumbprint { get; set; } - public string SerialNumber { get; set; } - public string IssuedDN { get; set; } - public string IssuedCN { get; set; } - public DateTime ImportDate { get; set; } - public DateTime NotBefore { get; set; } - public DateTime NotAfter { get; set; } - public string IssuerDN { get; set; } - public object PrincipalId { get; set; } - public int TemplateId { get; set; } - public int CertState { get; set; } - public int KeySizeInBits { get; set; } - public int KeyType { get; set; } - public object RequesterId { get; set; } - public object IssuedOU { get; set; } - public object IssuedEmail { get; set; } - public int KeyUsage { get; set; } - public string SigningAlgorithm { get; set; } - public string CertStateString { get; set; } - public string KeyTypeString { get; set; } - public object RevocationEffDate { get; set; } - public object RevocationReason { get; set; } - public object RevocationComment { get; set; } - public int CertificateAuthorityId { get; set; } - public string CertificateAuthorityName { get; set; } - public string TemplateName { get; set; } - public bool ArchivedKey { get; set; } - public bool HasPrivateKey { get; set; } - public object PrincipalName { get; set; } - public object CertRequestId { get; set; } - public object RequesterName { get; set; } - public string ContentBytes { get; set; } - public Extendedkeyusage[] ExtendedKeyUsages { get; set; } - public Subjectaltnameelement[] SubjectAltNameElements { get; set; } - public Crldistributionpoint[] CRLDistributionPoints { get; set; } - public object[] LocationsCount { get; set; } - public object[] SSLLocations { get; set; } - public object[] Locations { get; set; } - public Metadata Metadata { get; set; } - public int CertificateKeyId { get; set; } - public int CARowIndex { get; set; } - public Detailedkeyusage DetailedKeyUsage { get; set; } - public bool KeyRecoverable { get; set; } - } - - public class Metadata - { - - } - - public class Detailedkeyusage - { - public bool CrlSign { get; set; } - public bool DataEncipherment { get; set; } - public bool DecipherOnly { get; set; } - public bool DigitalSignature { get; set; } - public bool EncipherOnly { get; set; } - public bool KeyAgreement { get; set; } - public bool KeyCertSign { get; set; } - public bool KeyEncipherment { get; set; } - public bool NonRepudiation { get; set; } - public string HexCode { get; set; } - } - - public class Extendedkeyusage - { - public int Id { get; set; } - public string Oid { get; set; } - public string DisplayName { get; set; } - } - - public class Subjectaltnameelement - { - public int Id { get; set; } - public string Value { get; set; } - public int Type { get; set; } - public string ValueHash { get; set; } - } - - public class Crldistributionpoint - { - public int Id { get; set; } - public string Url { get; set; } - public string UrlHash { get; set; } - } -} \ No newline at end of file diff --git a/digicert-metadata-sync/Models/KeyfactorMetadataInstance.cs b/digicert-metadata-sync/Models/KeyfactorMetadataInstance.cs deleted file mode 100644 index 043c9ca..0000000 --- a/digicert-metadata-sync/Models/KeyfactorMetadataInstance.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2021 Keyfactor -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace DigicertMetadataSync; - -internal partial class DigicertSync -{ - //This stores all of the data keyfactor API returns when asked for metadata field details. - public class KeyfactorMetadataInstance - { - public int Id { get; set; } - public string Name { get; set; } - public string Description { get; set; } - public int DataType { get; set; } - public string Hint { get; set; } - public string Validation { get; set; } - public int Enrollment { get; set; } - public string Message { get; set; } - public string Options { get; set; } - public string DefaultValue { get; set; } - public bool AllowAPI { get; set; } - public bool ExplicitUpdate { get; set; } - public int DisplayOrder { get; set; } - } -} \ No newline at end of file diff --git a/digicert-metadata-sync/Models/KeyfactorMetadataQuery.cs b/digicert-metadata-sync/Models/KeyfactorMetadataQuery.cs deleted file mode 100644 index 4cc8dde..0000000 --- a/digicert-metadata-sync/Models/KeyfactorMetadataQuery.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2021 Keyfactor -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace DigicertMetadataSync; - -internal partial class DigicertSync -{ - public class KeyfactorMetadataQuery - { - public Dictionary Metadata = new(); - public int Id { get; set; } - } -} \ No newline at end of file diff --git a/digicert-metadata-sync/Models/KeyfactorModels.cs b/digicert-metadata-sync/Models/KeyfactorModels.cs new file mode 100644 index 0000000..bdb980b --- /dev/null +++ b/digicert-metadata-sync/Models/KeyfactorModels.cs @@ -0,0 +1,148 @@ +// Copyright 2024 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. +using System.Text.Json.Serialization; + +namespace DigicertMetadataSync.Models; + +public class KeyfactorMetadataField +{ + // ⬇ Add this attribute + [JsonPropertyName("Id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int Id { get; set; } // leave as 0 for create; set >0 for update + + [JsonPropertyName("Name")] public string Name { get; set; } = string.Empty; // Default to empty string + [JsonPropertyName("Description")] public string Description { get; set; } = string.Empty; // Default to empty string + [JsonPropertyName("DataType")] public int DataType { get; set; } = 0; // Default to 0 + [JsonPropertyName("Hint")] public string? Hint { get; set; } = null; // Nullable + [JsonPropertyName("Validation")] public string? Validation { get; set; } = null; // Nullable + [JsonPropertyName("Enrollment")] public int Enrollment { get; set; } = 0; // Default to 0 (Optional) + [JsonPropertyName("Message")] public string? Message { get; set; } = null; // Nullable + [JsonPropertyName("Options")] public string? Options { get; set; } = null; // Nullable + [JsonPropertyName("DefaultValue")] public string? DefaultValue { get; set; } = null; // Nullable + [JsonPropertyName("DisplayOrder")] public int DisplayOrder { get; set; } = 0; // Default to 0 + [JsonPropertyName("CaseSensitive")] public bool CaseSensitive { get; set; } = false; // Default to false +} + +/// +/// Represents the data types for Keyfactor metadata fields. +/// +public enum KeyfactorMetadataDataType +{ + Unknown = 0, + String = 1, + Integer = 2, + Date = 3, + Boolean = 4, + MultipleChoice = 5, + BigText = 6, + Email = 7 +} + +public class KeyfactorCertificate +{ + public int Id { get; set; } = 0; // Default to 0 + public string Thumbprint { get; set; } = string.Empty; // Default to empty string + public string SerialNumber { get; set; } = string.Empty; // Default to empty string + public string IssuedDN { get; set; } = string.Empty; // Default to empty string + public string IssuedCN { get; set; } = string.Empty; // Default to empty string + public DateTime ImportDate { get; set; } = DateTime.MinValue; // Default to MinValue + public DateTime NotBefore { get; set; } = DateTime.MinValue; // Default to MinValue + public DateTime NotAfter { get; set; } = DateTime.MinValue; // Default to MinValue + public string IssuerDN { get; set; } = string.Empty; // Default to empty string + public object PrincipalId { get; set; } = new(); // Default to new object + public int? OwnerRoleId { get; set; } = null; // Nullable + public string OwnerRoleName { get; set; } = string.Empty; // Default to empty string + public int? TemplateId { get; set; } = null; // Nullable + public int CertState { get; set; } = 0; // Default to 0 + public int KeySizeInBits { get; set; } = 0; // Default to 0 + public int KeyType { get; set; } = 0; // Default to 0 + public string KeyAlgorithm { get; set; } = string.Empty; // Default to empty string + public object AltKeyAlgorithm { get; set; } = new(); // Default to new object + public int AltKeySizeInBits { get; set; } = 0; // Default to 0 + public object AltKeyType { get; set; } = new(); // Default to new object + public int? RequesterId { get; set; } = null; // Nullable + public string IssuedOU { get; set; } = string.Empty; // Default to empty string + public string IssuedEmail { get; set; } = string.Empty; // Default to empty string + public int KeyUsage { get; set; } = 0; // Default to 0 + public string SigningAlgorithm { get; set; } = string.Empty; // Default to empty string + public object AltSigningAlgorithm { get; set; } = new(); // Default to new object + public string CertStateString { get; set; } = string.Empty; // Default to empty string + public string KeyTypeString { get; set; } = string.Empty; // Default to empty string + public object AltKeyTypeString { get; set; } = new(); // Default to new object + public DateTime? RevocationEffDate { get; set; } = null; // Nullable + public int? RevocationReason { get; set; } = null; // Nullable + public object RevocationComment { get; set; } = new(); // Default to new object + public int? CertificateAuthorityId { get; set; } = null; // Nullable + public string CertificateAuthorityName { get; set; } = string.Empty; // Default to empty string + public string TemplateName { get; set; } = string.Empty; // Default to empty string + public bool ArchivedKey { get; set; } = false; // Default to false + public bool HasPrivateKey { get; set; } = false; // Default to false + public bool HasAltPrivateKey { get; set; } = false; // Default to false + public string PrincipalName { get; set; } = string.Empty; // Default to empty string + public object CertRequestId { get; set; } = new(); // Default to new object + public string RequesterName { get; set; } = string.Empty; // Default to empty string + public string ContentBytes { get; set; } = string.Empty; // Default to empty string + + public Extendedkeyusage[] ExtendedKeyUsages { get; set; } = + Array.Empty(); // Default to empty array + + public Subjectaltnameelement[] SubjectAltNameElements { get; set; } = + Array.Empty(); // Default to empty array + + public Crldistributionpoint[] CRLDistributionPoints { get; set; } = + Array.Empty(); // Default to empty array + + public object[] LocationsCount { get; set; } = Array.Empty(); // Default to empty array + public object[] SSLLocations { get; set; } = Array.Empty(); // Default to empty array + public object[] Locations { get; set; } = Array.Empty(); // Default to empty array + + [JsonPropertyName("Metadata")] public Dictionary? Metadata { get; set; } = null; // Nullable + + public int? CARowIndex { get; set; } = null; // Nullable + public string CARecordId { get; set; } = string.Empty; // Default to empty string + public Detailedkeyusage DetailedKeyUsage { get; set; } = new(); // Default to new instance + public bool KeyRecoverable { get; set; } = false; // Default to false + public object Curve { get; set; } = new(); // Default to new object + public object EnrollmentPatternId { get; set; } = new(); // Default to new object +} + +public class Detailedkeyusage +{ + public bool CrlSign { get; set; } = false; // Default to false + public bool DataEncipherment { get; set; } = false; // Default to false + public bool DecipherOnly { get; set; } = false; // Default to false + public bool DigitalSignature { get; set; } = false; // Default to false + public bool EncipherOnly { get; set; } = false; // Default to false + public bool KeyAgreement { get; set; } = false; // Default to false + public bool KeyCertSign { get; set; } = false; // Default to false + public bool KeyEncipherment { get; set; } = false; // Default to false + public bool NonRepudiation { get; set; } = false; // Default to false + public string HexCode { get; set; } = string.Empty; // Default to empty string +} + +public class Extendedkeyusage +{ + public int Id { get; set; } = 0; // Default to 0 + public string Oid { get; set; } = string.Empty; // Default to empty string + public string DisplayName { get; set; } = string.Empty; // Default to empty string +} + +public class Subjectaltnameelement +{ + public int Id { get; set; } = 0; // Default to 0 + public string Value { get; set; } = string.Empty; // Default to empty string + public int Type { get; set; } = 0; // Default to 0 + public string ValueHash { get; set; } = string.Empty; // Default to empty string +} + +public class Crldistributionpoint +{ + public int Id { get; set; } = 0; // Default to 0 + public string Url { get; set; } = string.Empty; // Default to empty string + public string UrlHash { get; set; } = string.Empty; // Default to empty string +} \ No newline at end of file diff --git a/digicert-metadata-sync/NLog.config b/digicert-metadata-sync/NLog.config deleted file mode 100644 index 1ff0000..0000000 --- a/digicert-metadata-sync/NLog.config +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/digicert-metadata-sync/Properties/launchSettings.json b/digicert-metadata-sync/Properties/launchSettings.json new file mode 100644 index 0000000..9c2486b --- /dev/null +++ b/digicert-metadata-sync/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "dctokf": { + "commandName": "Project", + "commandLineArgs": "dctokf" + }, + "kftodc": { + "commandName": "Project", + "commandLineArgs": "kftodc" + } + } +} \ No newline at end of file diff --git a/digicert-metadata-sync/config/bannedcharacters.json b/digicert-metadata-sync/config/bannedcharacters.json new file mode 100644 index 0000000..f435a94 --- /dev/null +++ b/digicert-metadata-sync/config/bannedcharacters.json @@ -0,0 +1,4 @@ +{ + "BannedCharacters": [ + ] +} \ No newline at end of file diff --git a/digicert-metadata-sync/config/nlog.config b/digicert-metadata-sync/config/nlog.config new file mode 100644 index 0000000..d01c760 --- /dev/null +++ b/digicert-metadata-sync/config/nlog.config @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/digicert-metadata-sync/config/stock-config.json b/digicert-metadata-sync/config/stock-config.json new file mode 100644 index 0000000..82d8990 --- /dev/null +++ b/digicert-metadata-sync/config/stock-config.json @@ -0,0 +1,15 @@ +{ + "config": { + "digicertApiKey": "*****", + "keyfactorDomainAndUser": "DOMAIN\\username", + "keyfactorPassword": "*****", + "keyfactorAPIUrl": "https://keyfactor.test.com/keyfactorapi/", + "keyfactorDigicertIssuedCertQueryTerm": "DigiCert", + "importAllCustomDigicertFields": false, + "importDataForDeactivatedDigiCertFields": false, + "syncRevokedAndExpiredCerts": false, + "keyfactorPageSize": 100, + "keyfactorDateFormat": "M/d/yyyy h:mm:ss tt", + "createMissingFieldsInDigicert": true + } +} diff --git a/digicert-metadata-sync/config/stock-fields.json b/digicert-metadata-sync/config/stock-fields.json new file mode 100644 index 0000000..fc83652 --- /dev/null +++ b/digicert-metadata-sync/config/stock-fields.json @@ -0,0 +1,104 @@ +{ + "_comments": { + "ManualFields": "List of fields used for loading static information from DigiCert as certificate Metadata in Keyfactor.", + "CustomFields": "List of Custom Fields/Metadata Fields to be synced between Keyfactor and DigiCert.", + "digicertFieldName": "The name of the field in DigiCert. If manual field, an address into the DigiCert Certificate Details json response.", + "digicertDataType": "Corresponds to what the datatype is in DigiCert. Only applies to custom fields, used for automated field creation in DigiCert. Options available: 0=Anything, 1=Text, 2=Int, 3=EmailAddress, 4=EmailList. Drop down type is not suppported.", + "keyfactorMetadataFieldName": "The name of the field in Keyfactor. This must not contain spaces, or a variety of other characters. Only [a-zA-Z0-9-_] fitting characters are accepted.", + "keyfactorDescription": "A description of the metadata field for use with Keyfactor.", + "keyfactorDataType": "The data type of the field for Keyfactor. String = 1, Integer = 2, Date = 3, Boolean = 4, MultipleChoice = 5, BigText = 6, Email = 7.", + "keyfactorHint": "A short hint to guide users on what to enter in the field, displayed in Keyfactor.", + "keyfactorValidation": "A RegEx expression to validate the field's input. Only applicable for string fields.", + "keyfactorEnrollment": "How the metadata field is handled in Keyfactor during certificate enrollment. 0=Optional, 1=Required, 2=Hidden", + "keyfactorMessage": "A message to be displayed when validation fails.", + "keyfactorOptions": "An array of values for multiple-choice fields. Ignored for other data types. (e.g. ['option1','option2'])", + "keyfactorDefaultValue": "A default value for the field. Only applicable for certain data types.", + "keyfactorDisplayOrder": "The order in which the field is displayed.", + "keyfactorCaseSensitive": "Whether validation is case-sensitive. Only applicable for string fields with validation." + }, + "ManualFields": [ + { + "digicertFieldName": "id", + "keyfactorMetadataFieldName": "DigicertID", + "keyfactorDescription": "Digicert Assigned Cert ID", + "digicertDataType": null, + "keyfactorDataType": 2, + "keyfactorHint": null, + "keyfactorAllowAPI": "True", + "keyfactorValidation": null, + "keyfactorMessage": null, + "keyfactorEnrollment": 0, + "keyfactorOptions": null, + "keyfactorDefaultValue": null, + "keyfactorCaseSensitive": false, + "keyfactorDisplayOrder": 3 + }, + { + "digicertFieldName": "additional_emails", + "keyfactorMetadataFieldName": "additional_emails", + "keyfactorDescription": "Digicert Additional notification emails", + "keyfactorDataType": 7, + "digicertDataType": null, + "keyfactorHint": null, + "keyfactorAllowAPI": "True", + "keyfactorValidation": null, + "keyfactorMessage": null, + "keyfactorEnrollment": 0, + "keyfactorOptions": null, + "keyfactorDefaultValue": null, + "keyfactorCaseSensitive": false, + "keyfactorDisplayOrder": 3 + }, + { + "digicertFieldName": "certificate.organization.id", + "keyfactorMetadataFieldName": "certificate-organization-id", + "keyfactorDescription": "Digicert Assigned Org ID for cert", + "keyfactorDataType": 2, + "digicertDataType": null, + "keyfactorHint": null, + "keyfactorAllowAPI": "True", + "keyfactorValidation": null, + "keyfactorMessage": null, + "keyfactorEnrollment": 0, + "keyfactorOptions": null, + "keyfactorDefaultValue": null, + "keyfactorCaseSensitive": false, + "keyfactorDisplayOrder": 3 + } + ], + "CustomFields": [ + { + "digicertFieldName": "To DigiCert Date", + "keyfactorMetadataFieldName": "to_digicert_date", + "keyfactorDescription": "Date for digicert", + "keyfactorDataType": 3, + "digicertDataType": 0, + "keyfactorHint": null, + "keyfactorAllowAPI": "True", + "keyfactorValidation": null, + "keyfactorMessage": null, + "keyfactorEnrollment": 0, + "keyfactorOptions": null, + "keyfactorDefaultValue": null, + "keyfactorCaseSensitive": false, + "keyfactorDisplayOrder": 3 + }, + { + "digicertFieldName": "To DigiCert MC", + "keyfactorMetadataFieldName": "to_digicert_mc", + "keyfactorDescription": "Multiple choice for digicert", + "keyfactorDataType": 5, + "digicertDataType": 0, + "keyfactorHint": null, + "keyfactorAllowAPI": "True", + "keyfactorValidation": null, + "keyfactorMessage": null, + "keyfactorEnrollment": 0, + "keyfactorOptions": ["Option1","Option2"], + "keyfactorDefaultValue": null, + "keyfactorCaseSensitive": false, + "keyfactorDisplayOrder": 3 + } + ] + +} diff --git a/digicert-metadata-sync/digicertsync.Designer.cs b/digicert-metadata-sync/digicertsync.Designer.cs deleted file mode 100644 index 38e6af5..0000000 --- a/digicert-metadata-sync/digicertsync.Designer.cs +++ /dev/null @@ -1,38 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -// Copyright 2021 Keyfactor -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -namespace digicert_metadata_sync { - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.2.0.0")] - internal sealed partial class digicertsync : global::System.Configuration.ApplicationSettingsBase { - - private static digicertsync defaultInstance = ((digicertsync)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new digicertsync()))); - - public static digicertsync Default { - get { - return defaultInstance; - } - } - } -} diff --git a/digicert-metadata-sync/manualfields.json b/digicert-metadata-sync/manualfields.json deleted file mode 100644 index 099416c..0000000 --- a/digicert-metadata-sync/manualfields.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "ManualFields": [ - { - "DigicertFieldName": "id", - "KeyfactorMetadataFieldName": "DigicertID", - "KeyfactorDescription": "Digicert Assigned Cert ID", - "KeyfactorDataType": "Integer", - "KeyfactorHint": "", - "KeyfactorAllowAPI": "True" - }, - { - "DigicertFieldName": "organization_contact.email", - "KeyfactorMetadataFieldName": "digicertorgemail", - "KeyfactorDescription": "Digicert Requester Email", - "KeyfactorDataType": "String", - "KeyfactorHint": "", - "KeyfactorAllowAPI": "True" - } - ], - "CustomFields": [ - { - "DigicertFieldName": "test", - "KeyfactorMetadataFieldName": "", - "KeyfactorDescription": "Just an empty testfield.", - "KeyfactorDataType": "String", - "KeyfactorHint": "", - "KeyfactorAllowAPI": "True" - }, - { - "DigicertFieldName": "IntegerField", - "KeyfactorMetadataFieldName": "", - "KeyfactorDescription": "Another test field but with an int.", - "KeyfactorDataType": "Integer", - "KeyfactorHint": "", - "KeyfactorAllowAPI": "True" - } - ] -} diff --git a/digicert-metadata-sync/replacechar.json b/digicert-metadata-sync/replacechar.json deleted file mode 100644 index 0637a08..0000000 --- a/digicert-metadata-sync/replacechar.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/docsource/content.md b/docsource/content.md new file mode 100644 index 0000000..e6ed586 --- /dev/null +++ b/docsource/content.md @@ -0,0 +1,224 @@ +# DigiCert Metadata Sync Tool + +## Overview +### ⚠️ Important Notice +**Configuration files and their location have changed since version 2.1.0** Please review the documentation and see the new stock configuration files for guidance on how to set up the tool. The configuration files will need to be placed in the `config` subdirectory for use with the tool. + +This tool automates the synchronization of metadata fields between **DigiCert CertCentral** and **Keyfactor Command**. It performs two primary operations: + +1. **DCtoKF** - Synchronizes *manual fields* and *custom fields* from DigiCert into Keyfactor. +2. **KFtoDC** - Synchronizes *custom fields* from Keyfactor back into DigiCert. + +> **Notes** +> +> * **ManualFields** are values present in DigiCert's *Order Info* JSON and are mapped by dot path (e.g., `organization_contact.email`). Manual field data is available **only** for DigiCert -> Keyfactor sync. +> * **CustomFields** are DigiCert CertCentral custom fields and can be synchronized in **both** directions (DigiCert <--> Keyfactor). +> * The list of available **manual** fields is derived from the DigiCert *Order Info* API. See: [DigiCert Order Info API response](https://dev.digicert.com/en/certcentral-apis/services-api/orders/order-info.html) +> * Certificates must already exist in Keyfactor; this tool does **not** import certificates. + +--- + +## Installation and Usage + +### Prerequisites + +- .NET **9** (or newer) runtime. +- DigiCert **API key** with **API key restrictions (optional)** set to **None** when creating the key in CertCentral. +- A Keyfactor account with API access and permission to create/edit metadata fields and modify certificates. +- The following files in the **`config`** subdirectory: + - `config.json` + - `fields.json` + - `bannedcharacters.json` (auto-generated on first run if needed) + +Additional notes: + +- Designed for **Keyfactor 25.1**; tested compatible with older versions. +- The tool communicates directly with **Keyfactor Command API** and **DigiCert** - no Keyfactor Gateway dependency. +- Independent logging: logs are written to a local `logs/` folder next to the executable. + +### Running the Tool + +From the tool directory, open PowerShell and run: + +```powershell +./DigicertMetadataSync.exe dctokf +``` + +or + +```powershell +./DigicertMetadataSync.exe kftodc +``` + +> **Tip:** The tool performs one sync in the specified direction and then exits. Schedule it (e.g., with Windows Task Scheduler) for recurring syncs. + +--- + +## Command Line Modes + +One of the following modes must be supplied as the **first (and only) argument**: + +- `dctokf` + Synchronizes **manual** and **custom** fields **from DigiCert to Keyfactor**. + - Reads mappings from `fields.json` for manual fields. + - If `importAllCustomDigicertFields` is **true**, imports *all* DigiCert custom fields; otherwise uses only those listed under `CustomFields` in `fields.json`. + - Ensures required metadata fields exist in Keyfactor, creating missing ones. + - Locates DigiCert-issued certs in Keyfactor (by Issuer DN filter). + - Updates Keyfactor metadata with coerced values. + +- `kftodc` + Synchronizes **custom** fields **from Keyfactor to DigiCert**. + - Reads mappings from `fields.json` for custom fields. + - Ensures required metadata fields exist in Keyfactor. + - If `createMissingFieldsInDigicert` is **true** and `importAllCustomDigicertFields` is **false**, attempts to create missing DigiCert custom fields (limited by DigiCert API capabilities). + - Locates DigiCert-issued certs in Keyfactor (by Issuer DN filter). + - Updates DigiCert custom field values with coerced data types. + +> **Important:** Run `dctokf` at least once before running `kftodc` so Keyfactor metadata fields exist and have been normalized. + +--- + +## Settings + +### 1. `config\config.json` + +> See `stock-config.json` for a complete example. Please input `null` to set a value is empty. + +- **`digicertApiKey`** - CertCentral API key. Use a key created with **API key restrictions = None**. +- **`keyfactorDomainAndUser`** - e.g., `DOMAIN\\Username`. User must be permitted to use the Keyfactor API, create/edit metadata fields, and edit certificates. +- **`keyfactorPassword`** - Password for the Keyfactor user. +- **`keyfactorApiUrl`** - Root Keyfactor API URL, e.g., `https://your-keyfactor-server/keyfactorapi/`. +- **`keyfactorDigicertIssuedCertQueryTerm`** - Substring matched against Issuer DN to identify DigiCert‑issued certificates (e.g., `"DigiCert"`). +- **`importAllCustomDigicertFields`** - If `true`, import all DigiCert custom fields and auto-create Keyfactor metadata fields to match (ignores `CustomFields` entries). +- **`importDataForDeactivatedDigiCertFields`** - If `true`, process DigiCert fields even if deactivated. +- **`syncRevokedAndExpiredCerts`** - If `true`, include revoked and expired certificates in sync. +- **`keyfactorPageSize`** - Batch size for Keyfactor certificate processing (default: `100`). +- **`keyfactorDateFormat`** - Date format for Keyfactor writes (defaults vary by Keyfactor version; `M/d/yyyy h:mm:ss tt` for 25.1, `yyyy-MM-dd` for some older Keyfactor versions). +- **`createMissingFieldsInDigicert`** - If `true` (and `importAllCustomDigicertFields` is `false`), create missing DigiCert custom fields when syncing KF→DC (subject to DigiCert API limitations). + +--- + +### 2. `config\fields.json` + +> See `stock-fields.json` for examples. + +For each mapping: + +- **`digicertFieldName`** - DigiCert field name; for manual fields, a **dot path** into the Order Info JSON. +- **`digicertCustomFieldDataType`** - Input type for DigiCert **custom** fields: + `0` = Anything, `1` = Text, `2` = Int, `3` = EmailAddress, `4` = EmailList. + *(Dropdowns are not supported by the DigiCert API.)* +- **`keyfactorMetadataFieldName`** - Target Keyfactor metadata field name (**[A-Za-z0-9-_]** only; no spaces). +- **`keyfactorDescription`** - Description shown in Keyfactor. +- **`keyfactorDataType`** - Keyfactor type: `1` String, `2` Integer, `3` Date, `4` Boolean, `5` MultipleChoice, `6` BigText, `7` Email. +- **`keyfactorHint`** - UI hint text in Keyfactor. +- **`keyfactorValidation`** - Regex validation (string fields only). +- **`keyfactorEnrollment`** - Enrollment behavior (e.g., `0` Optional, `1` Required, `2` Hidden). +- **`keyfactorMessage`** - Validation failure message. +- **`keyfactorOptions`** - Values for MultipleChoice (ignored otherwise). +- **`keyfactorDefaultValue`** - Default value, if applicable. +- **`keyfactorDisplayOrder`** - Display order in Keyfactor. +- **`keyfactorCaseSensitive`** - Whether validation is case-sensitive (string fields with validation). + +Please review this for the exact values available for each `keyfactor` field: [Keyfactor API Reference](https://software.keyfactor.com/Core-OnPrem/v25.2/Content/WebAPI/KeyfactorAPI/MetadataFieldsPost.htm) + +--- + +### 3. `config\bannedcharacters.json` + +Generated on first `dctokf` run if DigiCert custom field names contain characters not permitted by Keyfactor (only alphanumeric, `-`, and `_` are allowed). Fill in `replacementCharacter` for each banned `character`, then re-run. + +**Example:** + +```jsonc +[ + { "character": " ", "replacementCharacter": "_" }, + { "character": "/", "replacementCharacter": "-" } +] +``` + +If any `replacementCharacter` remains `null`, the tool exits with an error on the next run. + +--- + +### 4. `config\nlog.config` (Logging) + +Logging uses **NLog** and writes to a local `logs/` folder. + +- Configure minimum levels and targets in `rules`. +- Two files are typically produced: a main log (all levels) and an error-only log. + +> Adjust `minLevel` in the `` section to change verbosity. Available levels: `Trace`, `Debug`, `Info`. `Info` for default. + +--- + +## Example Workflow + +1. **Initial Setup** + - Populate `config\config.json` with DigiCert and Keyfactor credentials and settings. + - Define `ManualFields` and `CustomFields` lists in `config\fields.json`. + +2. **First Run (Detect Banned Characters)** + ```powershell + ./DigicertMetadataSync.exe dctokf + ``` + - If banned characters are found in DigiCert custom field names, the tool logs a warning and exits. + - A `bannedcharacters.json` file is created with `replacementCharacter: null` entries. + +3. **Populate Replacements** + - Edit `config\bannedcharacters.json` and set `replacementCharacter` values. + - Save the file. + +4. **Second Run (Create Fields & Sync Data)** + ```powershell + ./DigicertMetadataSync.exe dctokf + ``` + - Fields are created/validated; data is synchronized DigiCert -> Keyfactor. + - (Optional) Run `kftodc` to push Keyfactor values back to DigiCert custom fields. + +--- + +## How It Works + +### DigiCert -> Keyfactor (`dctokf`) +1. Read manual mappings from `fields.json`. +2. Read custom fields from DigiCert (all if `importAllCustomDigicertFields` is `true`; otherwise only those listed). +3. Ensure Keyfactor metadata fields exist (create missing). +4. Query Keyfactor for DigiCert-issued certs (Issuer DN filter). +5. For each certificate: + - Fetch DigiCert order data (manual + custom). + - Coerce types to Keyfactor formats. + - Update Keyfactor metadata values. + +### Keyfactor -> DigiCert (`kftodc`) +1. Read custom field mappings from `fields.json`. +2. Ensure Keyfactor metadata fields exist (create missing). +3. If enabled, create missing DigiCert custom fields (API limitations apply). +4. Query Keyfactor for DigiCert-issued certs. +5. For each certificate: + - Read Keyfactor metadata values. + - Coerce to DigiCert data types. + - Update DigiCert custom field values on the order. + +**Retry logic:** When DigiCert rate-limits, the tool honors the DigiCert-supplied backoff time before retrying. + +--- + +## Usage Recommendations + +- Schedule periodic runs using Windows Task Scheduler (or equivalent). +- Run from the tool's directory; ensure the account can read/write the `config` folder. +- Sync is **destructive** for the destination side (values are overwritten in the destination of the chosen direction). +- Differential change tracking is **not** supported due to DigiCert and Keyfactor API limitations. + +--- + +## Troubleshooting + +- **Authentication errors** - Verify DigiCert API key and Keyfactor credentials/URL. +- **Keyfactor field name errors** - Ensure `bannedcharacters.json` replacements are set and valid. +- **Field creation failures** - Check Keyfactor logs for details; API errors may be non-specific. +- **Custom fields with options in DigiCert** - The DigiCert API cannot create dropdown/option fields; create these manually in CertCentral. + +--- + diff --git a/integration-manifest.json b/integration-manifest.json index 89601cd..7dd3278 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -1,11 +1,12 @@ { - "$schema": "https://keyfactor.github.io/integration-manifest-schema.json", + "$schema": "https://keyfactor.github.io/v2/integration-manifest-schema.json", "integration_type": "api-client", - "name": "Digicert Metadata Sync", + "name": "DigiCert Metadata Sync", "status": "production", - "description": "A tool to automatically synchronize metadata fields and their content from DigiCert to Keyfactor. This utility is indented to be used in conjunction with the Digicert AnyGateway and adds to the information already synchronized by the gateway.", - "link_github": true, - "update_catalog": true, + "description": "Digicert Metadata Sync Application", + "link_github": false, + "update_catalog": false, "support_level": "kf-community", - "release_dir": "digicert-metadata-sync\\bin\\Release\\net6.0 " -} + "release_project": "digicert-metadata-sync/DigicertMetadataSync.csproj", + "release_dir": "digicert-metadata-sync/bin/Release" +} \ No newline at end of file diff --git a/readme_source.md b/readme_source.md deleted file mode 100644 index e7d257b..0000000 --- a/readme_source.md +++ /dev/null @@ -1,77 +0,0 @@ - - -## Overview -This tool primarily sets up metadata fields in Keyfactor for the custom metadata fields in DigiCert, which are named as such, but can also setup metadata fields in Keyfactor for non-custom fields available in DigiCert and unavailable in Keyfactor by default, such as the Digicert Cert ID and the Organization contact. These fields are referred to as manual fields in the context of this tool. After setting up these fields, the tool proceeds to update the contents of these fields. This tool only adds metadata to certificates that have already been imported into Keyfactor. Additionally, this tool requires a properly installed and functioning AnyGateway configured to work with Keyfactor and Digicert. The latest update allows for syncronization of custom field contents from Keyfactor to DigiCert. New fields are created in Keyfactor and DigiCert to accomodate for this. - -## Installation and Usage -The tool comes as a Windows executable. The tool performs synchronization each time its run. For the tool to run automatically, it needs to be added as a scheduled process using Windows. The advised interval for running it is once per week. The files DigicertMetadataSync.dll.config and manualfields.json need to be present in the same directory as the tool for it to run correctly. The specific location from which the tool is ran does not matter, but it needs to have access to both the Keyfactor API endpoint as well as Digicert, and appropriate permissions for access to the configuration files. -An explanation for the settings found in these files is given below. - -## Command Line Arguments -One of these two arguments needs to be used for the tool to run. -- "kftodc" -Syncronizes the contents of custom fields listed in manualfields.json from Keyfactor to DigiCert. If the fields in manualfields.json do not exist in Keyfactor or DigiCert, they are created first. Example: ```.\DigicertMetadataSync.exe kftodc``` -- "dctokf" -Syncronizes the contents of both custom and non-custom fields from DigiCert to Keyfactor. The fields are listed in manualfields.json, and are created if necessary. -Example: ```.\DigicertMetadataSync.exe dctokf``` - -## Settings -The settings currently present in these files are shown as an example and need to be configured for your specific situation. -### DigicertMetadataSync.dll.config settings -- DigicertAPIKey -Standard DigiCert API access key. -- DigicertAPIKeyTopPerm -DigiCert API access key with restrictions set to "None" - required for sync from Keyfactor to DigiCert. -- KeyfactorDomainAndUser -Same credential as used when logging into Keyfactor Command. A different set of credentials can be used provided they have adequate access permissions. -- KeyfactorPassword -Password for the account used in the KeyfactorDomainAndUser field. -- KeyfactorCertSearchReturnLimit -This specifies the number of certs the tool will expect to receive from Keyfactor Command. Can be set to an arbitrarily large number for unlimited or to a smaller number for testing. -- KeyfactorAPIEndpoint -This should include the Keyfactor API endpoint, of the format https://domain.com/keyfactorapi/ -- KeyfactorDigicertIssuedCertQueryTerm -This should include the common prefix all DigiCert certs have in your Keyfactor instance. For example, "DigiCert" -- ImportAllCustomDigicertFields -This setting enables the tool to import all of the custom metadata fields included in DigiCert and sync all of their data. - -During the first run, the tool will scan the custom fields it will be importing for characters that are not supported in Keyfactor Metadata field names. -Each unsupported character will be shown in a file named "replacechar.json" and its replacement can be selected. If the values in the file are not populated, the tool will not run a second time. -- ImportDataForDeactivatedDigiCertFields -If this is enabled, custom metadata fields that were deactivated in DigiCert will also be synced, and the data stored in these fields in certificates will be too. - -### replacechar.json settings -This file is populated during the first run of the tool if the ImportAllCustomDigicertFields setting is toggled. -The only text that needs replacing is shown as "null", and can be filled with any alphanumeric string. The "_" and "-" characters are also supported. - - -### manualfields.json settings -This file is used to specify which metadata fields should be synced up. - -The "ManualFields" section is used to specify the non custom fields to import into Keyfactor. - -The "CustomFields" section is used to specify which of the custom metadata fields in DigiCert should be imported into Keyfactor. - -- DigicertFieldName -For "ManualFields", this should specify the location and name of the field in the json returned from the DigiCert API following a certificate order query. If the field is not at the top level, the input should be delimited using a "." character: "organization_contact.email". The structure of the json the API returns can be viewed here: https://dev.digicert.com/services-api/orders/order-info/ -For "CustomFields", this should be the label of the custom metadata field as listed in DigiCert. - -- KeyfactorMetadataFieldName -This is the string that will be used as the field name in Keyfactor. -For "ManualFields", this needs to be configured. -For "CustomFields", if left blank, will use the same name as the same string as the DigicertFieldName, provided it has no spaces. - -- KeyfactorDescription -This is the string that will be setup as the field description in Keyfactor. - -- KeyfactorDataType -The datatype the field will use in Keyfactor. Currently accepted types are Int and String. - -- KeyfactorDataType -String to be input into Keyfactor as the metadata field hint. - -- KeyfactorAllowAPI -Allows API management of this metadata field in Keyfactor. Should be set to true for continuous synchronization with this tool. - -### Logging -Logging functionality can be configured via entering either "Debug" or "Trace" into the value of `` in NLog.config.