Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflow-readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The "Build PR on Dev" pipeline will be triggered when it identified pull request

- Squash merge the tracking pull request to main
- Create the release on GitHub from main branch
- Create the new release branch from main branch (this is done automatically by pipeline create-release.yaml)
- Create the new release branch from main branch
- Change the new release branch as the default branch in the repo and update the branch protection rules https://github.com/bcgov/itvr/settings/branches
- Update frontend/package.json
- version
Expand Down
2 changes: 1 addition & 1 deletion django/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM --platform=linux/amd64 python:3.9.1
FROM python:3.9.1

ENV PYTHONUNBUFFERED=1

Expand Down
4 changes: 4 additions & 0 deletions django/api/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@ def ready(self):
schedule_get_ncda_redeemed_rebates,
schedule_expire_expired_applications,
schedule_send_expiry_emails,
schedule_send_to_cra,
schedule_retrieve_from_cra,
)

if settings.RUN_JOBS and "qcluster" in sys.argv:
schedule_send_rebates_to_ncda()
schedule_get_ncda_redeemed_rebates()
schedule_expire_expired_applications()
schedule_send_expiry_emails()
schedule_send_to_cra()
schedule_retrieve_from_cra()


class ITVRAdminConfig(AdminConfig):
Expand Down
26 changes: 26 additions & 0 deletions django/api/migrations/0023_crafiletracker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 4.0.9 on 2025-09-05 03:31

from django.db import migrations, models
import django_extensions.db.fields


class Migration(migrations.Migration):

dependencies = [
('api', '0022_legacygoelectricrebateapplication'),
]

operations = [
migrations.CreateModel(
name='CraFileTracker',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
('sequence_number', models.IntegerField()),
],
options={
'db_table': 'cra_file_tracker',
},
),
]
1 change: 1 addition & 0 deletions django/api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
from . import go_electric_rebate_application
from . import household_member
from . import go_electric_rebate
from . import cra_file_tracker
11 changes: 11 additions & 0 deletions django/api/models/cra_file_tracker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django_extensions.db.models import TimeStampedModel
from django.db.models import (
IntegerField,
)


class CraFileTracker(TimeStampedModel):
sequence_number = IntegerField()

class Meta:
db_table = "cra_file_tracker"
26 changes: 26 additions & 0 deletions django/api/scheduled_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,29 @@ def schedule_send_expiry_emails():
)
except IntegrityError:
pass


def schedule_send_to_cra():
try:
schedule(
"api.tasks.send_to_cra",
name="send_to_cra",
schedule_type="C",
cron="00 * * * *",
q_options={"timeout": 900, "ack_failure": True},
)
except IntegrityError:
pass


def schedule_retrieve_from_cra():
try:
schedule(
"api.tasks.retrieve_from_cra",
name="retrieve_from_cra",
schedule_type="C",
cron="30 * * * *",
q_options={"timeout": 900, "ack_failure": True},
)
except IntegrityError:
pass
49 changes: 44 additions & 5 deletions django/api/services/cra.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import requests
import json
import io
from unidecode import unidecode
from django.conf import settings
from api.services.minio import get_minio_object
import paramiko


def read(file):
Expand Down Expand Up @@ -42,9 +45,8 @@
return results


def write(
data, today="20220516", program_code="BCVR", cra_env="A", cra_sequence="00001"
):
def write(data, today="20220516", program_code="BCVR", cra_env="A", cra_sequence=1):
sequence_number = (str(cra_sequence)).rjust(5, "0")
file = ""

# Number of records to write.
Expand All @@ -58,7 +60,7 @@
file += today #
file += " " # Blank space

file += program_code + cra_env + cra_sequence
file += program_code + cra_env + sequence_number

file += " " * 99 # Blank space

Expand All @@ -82,7 +84,7 @@
file += today # Request date
file += " " # Blank space

file += program_code + cra_env + cra_sequence
file += program_code + cra_env + sequence_number

file += " " * 6 # Blank space

Expand Down Expand Up @@ -124,3 +126,40 @@
)
response.raise_for_status()
return response.content


def get_to_cra_filename(program_code="BCVR", cra_env="A", cra_sequence=1, directory=""):
filename = "{directory}/TO.{cra_env}TO#@@00.R7005.IN.{program_code}.{cra_env}{cra_sequence:05}.p7m".format(
directory=directory,
cra_env=cra_env,
cra_sequence=cra_sequence,
program_code=program_code,
)
return filename


def get_from_cra_filename(
program_code="BCVR", cra_env="A", cra_sequence=1, directory=""
):
filename = "{directory}/{cra_env}{program_code}{cra_sequence:05}.p7m".format(
directory=directory,
cra_env=cra_env,
cra_sequence=cra_sequence,
program_code=program_code,
)
return filename


def get_ssh_client():
pkey_file = get_minio_object(settings.SSH_PKEY_FILENAME)
pkey = paramiko.RSAKey.from_private_key(io.StringIO(pkey_file.data.decode("utf-8")))
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy)

Check failure

Code scanning / CodeQL

Accepting unknown SSH host keys when using Paramiko High

Setting missing host key policy to AutoAddPolicy may be unsafe.

Copilot Autofix

AI 5 months ago

To fix this issue, you should change the missing host key policy in the get_ssh_client() function from paramiko.AutoAddPolicy to paramiko.RejectPolicy. The RejectPolicy will refuse connection if the host key is not present in the known hosts file, ensuring host authenticity and protecting against man-in-the-middle exploits. This fix should be implemented by changing line 157 in django/api/services/cra.py so that it reads client.set_missing_host_key_policy(paramiko.RejectPolicy).

If this change causes exceptions upon connecting to new servers, you'll need to ensure that host keys are preloaded into the system's known hosts (or manually added via Paramiko's load_system_host_keys or load_host_keys methods). However, based solely on the code provided, only the policy line needs to be updated.

Suggested changeset 1
django/api/services/cra.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/django/api/services/cra.py b/django/api/services/cra.py
--- a/django/api/services/cra.py
+++ b/django/api/services/cra.py
@@ -154,7 +154,7 @@
     pkey_file = get_minio_object(settings.SSH_PKEY_FILENAME)
     pkey = paramiko.RSAKey.from_private_key(io.StringIO(pkey_file.data.decode("utf-8")))
     client = paramiko.SSHClient()
-    client.set_missing_host_key_policy(paramiko.AutoAddPolicy)
+    client.set_missing_host_key_policy(paramiko.RejectPolicy)
     client.connect(
         settings.CRA_SFTP_HOST,
         port=int(settings.CRA_SFTP_PORT),
EOF
@@ -154,7 +154,7 @@
pkey_file = get_minio_object(settings.SSH_PKEY_FILENAME)
pkey = paramiko.RSAKey.from_private_key(io.StringIO(pkey_file.data.decode("utf-8")))
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy)
client.set_missing_host_key_policy(paramiko.RejectPolicy)
client.connect(
settings.CRA_SFTP_HOST,
port=int(settings.CRA_SFTP_PORT),
Copilot is powered by AI and may make mistakes. Always verify output.
client.connect(
settings.CRA_SFTP_HOST,
port=int(settings.CRA_SFTP_PORT),
username=settings.CRA_SFTP_USERNAME,
pkey=pkey,
disabled_algorithms={"pubkeys": ["rsa-sha2-256", "rsa-sha2-512"]},
)
return client
16 changes: 16 additions & 0 deletions django/api/services/minio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from minio import Minio
from django.conf import settings


def get_minio_client():
return Minio(
settings.MINIO_ENDPOINT_INTERNAL,
settings.MINIO_ACCESS_KEY,
settings.MINIO_SECRET_KEY,
secure=(settings.MINIO_USE_SSL == "True"),
)


def get_minio_object(object_name):
client = get_minio_client()
return client.get_object(settings.MINIO_BUCKET_NAME, object_name)
31 changes: 31 additions & 0 deletions django/api/services/rebate.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,34 @@ def update_application_statuses(rebates, applications):
created=False,
update_fields={"status"},
)


def get_rebate_data_for_cra():
result = []
rebates = GoElectricRebateApplication.objects.filter(
status=GoElectricRebateApplication.Status.VERIFIED
)
for rebate in rebates:
result.append(
{
"sin": rebate.sin,
"years": [rebate.tax_year],
"given_name": rebate.first_name,
"family_name": rebate.last_name,
"birth_date": rebate.date_of_birth.strftime("%Y%m%d"),
"application_id": rebate.id,
}
)
if rebate.application_type == "household":
household_member = rebate.householdmember
result.append(
{
"sin": household_member.sin,
"years": [rebate.tax_year],
"given_name": household_member.first_name,
"family_name": household_member.last_name,
"birth_date": household_member.date_of_birth.strftime("%Y%m%d"),
"application_id": rebate.id,
}
)
return result
12 changes: 10 additions & 2 deletions django/api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@
MINIO_SECRET_KEY = os.getenv("MINIO_ROOT_PASSWORD")
MINIO_BUCKET_NAME = os.getenv("MINIO_BUCKET_NAME")
MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT")
MINIO_ENDPOINT_INTERNAL = os.getenv("MINIO_ENDPOINT_INTERNAL")
MINIO_USE_SSL = os.getenv("MINIO_USE_SSL", "True")

DEFAULT_AUTO_FIELD = "django.db.models.AutoField"

Expand Down Expand Up @@ -227,7 +229,7 @@
# NCDA Sharepoint config
NCDA_CLIENT_ID = os.getenv(
"NCDA_CLIENT_ID",
"d4d97d40-bb26-44f8-ba70-c677471d6cc1@1d4864aa-f2da-42dc-a62a-34b4dd790b6a",
"d7a76f01-ddff-43a3-98d4-e20a3c52a0e5@1d4864aa-f2da-42dc-a62a-34b4dd790b6a",
)
NCDA_CLIENT_SECRET = os.getenv("NCDA_CLIENT_SECRET")
NCDA_RESOURCE = os.getenv(
Expand All @@ -251,9 +253,15 @@
CLAMD_HOST = os.getenv("CLAMD_HOST", "clamav")
CLAMD_PORT = int(os.getenv("CLAMD_PORT", 3310))

USE_CRYPTO_SERVICE = os.getenv("USE_CRYPTO_SERVICE", False)
CRYPTO_SERVICE_URL = os.getenv("CRYPTO_SERVICE_URL", "http://spring:8080")
CRA_CERTIFICATE = os.getenv("CRA_CERTIFICATE")
CRA_CERTIFICATE_CRL_DN = os.getenv("CRA_CERTIFICATE_CRL_DN")
EPF_FILENAME = os.getenv("EPF_FILENAME")
EPF_PASSWORD = os.getenv("EPF_PASSWORD")
SSH_PKEY_FILENAME = os.getenv("SSH_PKEY_FILENAME")
CRA_SFTP_HOST = os.getenv("CRA_SFTP_HOST")
CRA_SFTP_PORT = os.getenv("CRA_SFTP_PORT")
CRA_SFTP_USERNAME = os.getenv("CRA_SFTP_USERNAME")
CRA_SFTP_GET_PATH = os.getenv("CRA_SFTP_GET_PATH")
CRA_SFTP_PUT_PATH = os.getenv("CRA_SFTP_PUT_PATH")
CRA_PROGRAM_CODE = os.getenv("CRA_PROGRAM_CODE")
Loading
Loading