Skip to content

Conversation

@deacon-mp
Copy link

Description

Static pdf build of the training certificate.

export TRAINING_CERT_BYPASS=1
python3 server.py --insecure
DL=$(jq -r '.download' /tmp/issue.json)
curl -s -H "KEY: ADMIN123" -o cert-link.pdf "http://localhost:8888${DL// /%20}"
curl -s -X POST http://localhost:8888/plugin/training/certificate/issue -H "KEY: ADMIN123" -H "Content-Type: application/json" -d '{"certificate":"User Certificate","name":"Test User"}' | tee /tmp/issue.json

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Please describe the tests that you ran to verify your changes.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements a new certificate issuance system for the training plugin, replacing the previous certificate code-based completion system with direct PDF generation and download functionality.

  • Adds new API endpoints for certificate issuance, download, and reset operations
  • Replaces the certificate code display with a modal-based certificate generation UI
  • Implements a complete certificate service for PDF generation with MITRE Caldera branding

Reviewed Changes

Copilot reviewed 4 out of 8 changed files in this pull request and generated 4 comments.

File Description
hook.py Adds new API route registrations for certificate operations
gui/views/training.vue Replaces certificate code UI with PDF generation modal and download functionality
app/training_api.py Implements certificate API endpoints with completion validation and PDF generation
app/c_certificate_service.py New service class for PDF certificate generation using ReportLab

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines 100 to 103
if os.path.exists(FONT_REGULAR):
pdfmetrics.registerFont(ttfonts.TTFont('CertRegular', FONT_REGULAR))
if os.path.exists(FONT_BOLD):
pdfmetrics.registerFont(ttfonts.TTFont('CertBold', FONT_BOLD))
Copy link

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variables FONT_REGULAR and FONT_BOLD are referenced but never defined in this file, which will cause NameError exceptions when _register_fonts_if_available is called.

Copilot uses AI. Check for mistakes.
Comment on lines 153 to 155
if not cert_name or not display_name:
raise web.HTTPBadRequest(text='certificate and name required')

Copy link

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This validation check is duplicated from lines 144-145. The duplicate check should be removed to avoid redundant validation logic.

Suggested change
if not cert_name or not display_name:
raise web.HTTPBadRequest(text='certificate and name required')
# (Duplicate validation removed)

Copilot uses AI. Check for mistakes.
Comment on lines +198 to +199
raise web.HTTPNotFound(text='Certificate does not exist')
cert_id = getattr(cert, 'unique', None) or getattr(cert, 'identifier', None) or getattr(cert, 'id', None) or cert.name
Copy link

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cert_id assignment on line 199 is duplicated from line 196. This logic should be extracted to avoid code duplication.

Copilot uses AI. Check for mistakes.
(flag) => flag.badge_name === badgeList.value[badgeIndex - 1].name
);
if (!earlierFlags[earlierFlags.length - 1].completed) {
if (!earlierFlags.length || !earlierFlags[earlierFlags.length - 1].completed) {
Copy link

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The added check for !earlierFlags.length could cause issues if earlierFlags is an empty array but the logic expects to access the last element. Consider using optional chaining or a more explicit check.

Suggested change
if (!earlierFlags.length || !earlierFlags[earlierFlags.length - 1].completed) {
if (!earlierFlags.length || !earlierFlags[earlierFlags.length - 1]?.completed) {

Copilot uses AI. Check for mistakes.
@deacon-mp deacon-mp requested a review from Copilot October 6, 2025 23:10
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 4 out of 8 changed files in this pull request and generated 4 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +13 to +14
LOGO_TOP_CENTER = 'plugins/training/static/templates/mitreCaldera.png'
LOGO_BOTTOM_LEFT = 'plugins/training/static/templates/caldera_logo.png'
Copy link

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spacing in variable names for consistency.

Suggested change
LOGO_TOP_CENTER = 'plugins/training/static/templates/mitreCaldera.png'
LOGO_BOTTOM_LEFT = 'plugins/training/static/templates/caldera_logo.png'
LOGO_TOP_CENTER = 'plugins/training/static/templates/mitreCaldera.png'
LOGO_BOTTOM_LEFT = 'plugins/training/static/templates/caldera_logo.png'

Copilot uses AI. Check for mistakes.
out_pdf = os.path.join(OUT_DIR, base)

# Visible strings
display_cert_title = "MITRE Caldera™ User"
Copy link

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded certificate title should be configurable or derived from the cert_name parameter to support different certificate types.

Suggested change
display_cert_title = "MITRE Caldera™ User"
display_cert_title = cert_name

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@deacon-mp thoughts on this?

Comment on lines +68 to +71
const byteChars = atob(data.pdf_bytes);
const byteNums = new Array(byteChars.length);
for (let i = 0; i < byteChars.length; i++) byteNums[i] = byteChars.charCodeAt(i);
const blob = new Blob([new Uint8Array(byteNums)], { type: 'application/pdf' });
Copy link

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manual byte array conversion is inefficient. Consider using Uint8Array.from() for better performance: const byteArray = Uint8Array.from(atob(data.pdf_bytes), c => c.charCodeAt(0));

Suggested change
const byteChars = atob(data.pdf_bytes);
const byteNums = new Array(byteChars.length);
for (let i = 0; i < byteChars.length; i++) byteNums[i] = byteChars.charCodeAt(i);
const blob = new Blob([new Uint8Array(byteNums)], { type: 'application/pdf' });
const byteArray = Uint8Array.from(atob(data.pdf_bytes), c => c.charCodeAt(0));
const blob = new Blob([byteArray], { type: 'application/pdf' });

Copilot uses AI. Check for mistakes.
if not cert_name or not display_name:
raise web.HTTPBadRequest(text='certificate and name required')

user_id = request.headers.get('X-User-ID') or request.headers.get('KEY') or 'unknown'
Copy link

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user ID extraction logic is duplicated across multiple methods. Consider extracting this to the existing _request_user_id method.

Suggested change
user_id = request.headers.get('X-User-ID') or request.headers.get('KEY') or 'unknown'
user_id = await self._request_user_id(request)

Copilot uses AI. Check for mistakes.
except web.HTTPException:
raise
except Exception:
logging.exception("issue_certificate failed")
Copy link
Contributor

@uruwhy uruwhy Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we log the exception details?

if not cert_name:
raise web.HTTPBadRequest(text='certificate required')
user_id = body.get('user_id') or self._request_user_id(request)
instance_id = hashlib.sha256(self.cert_service.secret).hexdigest()[:12]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

best to turn this into a helper function since we're calling this logic at least 3 times

raise web.HTTPBadRequest(text='certificate and name required')

# auth-ish identity; same as issue_certificate
user_id = request.headers.get('X-User-ID') or request.headers.get('KEY') or 'unknown'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace with helper func

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants