Skip to content

Commit cac1aad

Browse files
committed
Merge branch 'dev_theo'
2 parents 3156866 + 8747a7c commit cac1aad

File tree

19 files changed

+2573
-796
lines changed

19 files changed

+2573
-796
lines changed

website/db_class/db.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,52 @@ def to_share(self):
134134
"share_url": f"http://cti-transmute.org/convert/share/{self.uuid}",
135135
"detail_url": f"http://cti-transmute.org/convert/detail/{self.id}"
136136
}
137+
138+
class ConvertHistory(db.Model):
139+
__tablename__ = "convert_history"
140+
141+
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
142+
user_id = db.Column(db.Integer, nullable=True)
143+
144+
convert_id = db.Column(
145+
db.Integer,
146+
db.ForeignKey("convert.id", ondelete="CASCADE"),
147+
nullable=False
148+
)
149+
150+
version = db.Column(db.Integer, nullable=False)
151+
uuid = db.Column(db.String(36), index=True, nullable=False)
152+
status = db.Column(db.String(20), nullable=False) # pending , accepted , rejected
153+
public = db.Column(db.Boolean, default=False, index=True) # able to share with the community
154+
155+
input_text = db.Column(db.Text, nullable=True)
156+
157+
old_output_text = db.Column(db.Text, nullable=True)
158+
new_output_text = db.Column(db.Text, nullable=True)
159+
160+
# Metadata
161+
created_at = db.Column(db.DateTime, index=True)
162+
comment = db.Column(db.Text, nullable=True)
163+
164+
# Relationship
165+
convert = db.relationship("Convert", backref=db.backref(
166+
"history",
167+
lazy=True,
168+
cascade="all, delete-orphan"
169+
))
170+
171+
def to_json(self):
172+
return {
173+
"id": self.id,
174+
"convert_id": self.convert_id,
175+
"version": self.version,
176+
"uuid": self.uuid,
177+
"input_text": self.input_text,
178+
"old_output_text": self.old_output_text,
179+
"new_output_text": self.new_output_text,
180+
"created_at": self.created_at.strftime('%Y-%m-%d %H:%M') if self.created_at else None,
181+
"comment": self.comment,
182+
"status": self.status,
183+
"public": self.public
184+
}
185+
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""empty message
2+
3+
Revision ID: d7053174c298
4+
Revises: a4ecea6f4ec3
5+
Create Date: 2025-11-26 10:12:07.474094
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = 'd7053174c298'
14+
down_revision = 'a4ecea6f4ec3'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.create_table('convert_history',
22+
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
23+
sa.Column('user_id', sa.Integer(), nullable=True),
24+
sa.Column('convert_id', sa.Integer(), nullable=False),
25+
sa.Column('version', sa.Integer(), nullable=False),
26+
sa.Column('uuid', sa.String(length=36), nullable=False),
27+
sa.Column('status', sa.String(length=20), nullable=False),
28+
sa.Column('public', sa.Boolean(), nullable=True),
29+
sa.Column('input_text', sa.Text(), nullable=True),
30+
sa.Column('old_output_text', sa.Text(), nullable=True),
31+
sa.Column('new_output_text', sa.Text(), nullable=True),
32+
sa.Column('created_at', sa.DateTime(), nullable=True),
33+
sa.Column('comment', sa.Text(), nullable=True),
34+
sa.ForeignKeyConstraint(['convert_id'], ['convert.id'], ondelete='CASCADE'),
35+
sa.PrimaryKeyConstraint('id')
36+
)
37+
with op.batch_alter_table('convert_history', schema=None) as batch_op:
38+
batch_op.create_index(batch_op.f('ix_convert_history_created_at'), ['created_at'], unique=False)
39+
batch_op.create_index(batch_op.f('ix_convert_history_public'), ['public'], unique=False)
40+
batch_op.create_index(batch_op.f('ix_convert_history_uuid'), ['uuid'], unique=False)
41+
42+
# ### end Alembic commands ###
43+
44+
45+
def downgrade():
46+
# ### commands auto generated by Alembic - please adjust! ###
47+
with op.batch_alter_table('convert_history', schema=None) as batch_op:
48+
batch_op.drop_index(batch_op.f('ix_convert_history_uuid'))
49+
batch_op.drop_index(batch_op.f('ix_convert_history_public'))
50+
batch_op.drop_index(batch_op.f('ix_convert_history_created_at'))
51+
52+
op.drop_table('convert_history')
53+
# ### end Alembic commands ###

website/version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.1.0

website/web/convert/convert.py

Lines changed: 247 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# website/web/convert/views.py
2+
import io
23
import json
34
from flask import Blueprint, jsonify, redirect, render_template, request, flash, url_for
45
from flask_login import current_user, login_required
@@ -128,7 +129,7 @@ def stix_to_misp():
128129
if form.single_event.data:
129130
params["single_event"] = ""
130131

131-
print(params)
132+
#print(params)
132133

133134
try:
134135
response = requests.post(
@@ -439,3 +440,248 @@ def share_convert():
439440
return redirect(url_for("convert.history"))
440441

441442
return render_template("convert/detail.html", convert=convert)
443+
444+
445+
446+
###########################
447+
# Refresh a conversion #
448+
###########################
449+
450+
@convert_blueprint.route("/refresh/<string:uuid>", methods=['GET', 'POST'])
451+
def refresh(uuid):
452+
453+
convert_obj = ConvertModel.get_convert_by_uuid(uuid)
454+
455+
if not convert_obj:
456+
flash("Conversion not found.", "danger")
457+
return redirect(url_for("convert.history"))
458+
459+
# Choose which WTForm to use
460+
if convert_obj.conversion_type == "MISP_TO_STIX":
461+
print("Using MISP to STIX form")
462+
form = mispToStixParamForm()
463+
elif convert_obj.conversion_type == "STIX_TO_MISP":
464+
print("Using STIX to MISP form")
465+
form = stixToMispParamForm()
466+
else:
467+
flash("Unsupported conversion type.", "danger")
468+
return redirect(url_for("convert.history"))
469+
470+
# Prefill form (GET)
471+
if request.method == "GET":
472+
form.name.data = convert_obj.name
473+
form.description.data = convert_obj.description
474+
form.public.data = convert_obj.public
475+
476+
result = None
477+
diff = None
478+
error = None
479+
480+
if form.validate_on_submit():
481+
482+
# Call the generic dispatcher
483+
new_output, is_identical, error_msg = ConvertModel.reconvert_conversion(convert_obj, form)
484+
485+
if error_msg:
486+
error = error_msg
487+
flash(error_msg, "danger")
488+
else:
489+
if not is_identical:
490+
flash("Conversion re-executed successfully! Changes detected.", "success")
491+
result = new_output
492+
diff = "The new conversion result is DIFFERENT from the previous one."
493+
else:
494+
flash("Conversion re-executed successfully! No changes detected.", "success")
495+
result = new_output
496+
diff = "The new conversion result is IDENTICAL to the previous one."
497+
498+
return render_template(
499+
"convert/refresh.html",
500+
form=form,
501+
convert_obj=convert_obj,
502+
result=result,
503+
diff=diff,
504+
error=error,
505+
filename=f"{convert_obj.name}_refresh.json"
506+
)
507+
508+
509+
# get_history
510+
511+
@convert_blueprint.route("/get_history", methods=['GET'])
512+
def get_history():
513+
id = request.args.get('id', 1, type=int)
514+
if id:
515+
convert_obj = ConvertModel.get_convert(id)
516+
if convert_obj:
517+
latest_history = ConvertModel.get_history_list(convert_obj.id)
518+
if latest_history:
519+
return {
520+
"success": True,
521+
"history_convert": [h.to_json() for h in latest_history],
522+
"message": "New convert found",
523+
"toast_class" : "success"
524+
}, 200
525+
return {
526+
"success": True,
527+
"message": "No conversion history found for this convert",
528+
"toast_class" : "danger"
529+
}, 200
530+
return {
531+
"success": False,
532+
"message": "No convert found for this id",
533+
"toast_class" : "danger"
534+
}, 500
535+
return {
536+
"success": False,
537+
"message": "No id provided",
538+
"toast_class" : "danger"
539+
}, 500
540+
541+
542+
543+
544+
@convert_blueprint.route("/get_new_convert", methods=['GET'])
545+
def get_new_convert():
546+
"""Get the new convert after a refresh to show the difference"""
547+
id = request.args.get('id', 1, type=int)
548+
if id:
549+
convert_obj = ConvertModel.get_convert(id)
550+
if convert_obj:
551+
latest_history = ConvertModel.get_latest_history_list(convert_obj.id)
552+
if latest_history:
553+
return {
554+
"success": True,
555+
"history_convert": [h.to_json() for h in latest_history],
556+
"message": "New convert found",
557+
"toast_class" : "success"
558+
}, 200
559+
return {
560+
"success": True,
561+
"message": "No conversion history found for this convert",
562+
"toast_class" : "danger"
563+
}, 200
564+
return {
565+
"success": False,
566+
"message": "No convert found for this id",
567+
"toast_class" : "danger"
568+
}, 500
569+
return {
570+
"success": False,
571+
"message": "No id provided",
572+
"toast_class" : "danger"
573+
}, 500
574+
575+
576+
@convert_blueprint.route("/history_action", methods=['GET'])
577+
def history_action():
578+
"""Handle actions related to conversion history"""
579+
action = request.args.get('action')
580+
history_id = request.args.get('history_id', type=int)
581+
convert_id = request.args.get('convert_id', type=int)
582+
583+
if current_user.is_anonymous():
584+
return {
585+
"success": False,
586+
"message": "You must be logged in to perform this action.",
587+
"toast_class": "danger"
588+
}, 401
589+
if not current_user.is_admin() and current_user.id != ConvertModel.get_convert(convert_id).user_id:
590+
return {
591+
"success": False,
592+
"message": "You do not have permission to perform this action.",
593+
"toast_class": "danger"
594+
}, 403
595+
else:
596+
# --- Handle Conversion History Actions (Accept/Reject) ---
597+
if history_id and action in ["accept", "reject"]:
598+
599+
if action == "accept":
600+
success = ConvertModel.accept_history(history_id)
601+
elif action == "reject":
602+
success = ConvertModel.reject_history(history_id)
603+
success = True # Replace with actual database call
604+
605+
if success:
606+
flash("Convert history updated", "success")
607+
return {
608+
"success": True,
609+
"message": f"History entry {history_id} {action}ed successfully.",
610+
"toast_class": "success"
611+
}, 200
612+
return {
613+
"success": False,
614+
"message": f"Failed to {action} history entry {history_id}.",
615+
"toast_class": "danger"
616+
}, 500
617+
618+
# --- Default Error ---
619+
return {
620+
"success": False,
621+
"message": "Invalid action or missing parameters (history_id or convert_id).",
622+
"toast_class": "danger"
623+
}, 400
624+
625+
626+
@convert_blueprint.route("/difference/<int:id>", methods=['GET'])
627+
def difference(id):
628+
"""Show the difference between two convert versions"""
629+
convert_obj_history = ConvertModel.get_convert_history_by_id(id)
630+
if not convert_obj_history:
631+
flash("Conversion not found.", "danger")
632+
return redirect(url_for("convert.history"))
633+
634+
convert_obj = ConvertModel.get_convert(convert_obj_history.convert_id)
635+
if not convert_obj:
636+
flash("Conversion not found.", "danger")
637+
return redirect(url_for("convert.history"))
638+
639+
if convert_obj.public == False:
640+
if current_user.is_anonymous():
641+
flash("You must be logged in to view this convert if you are the owner of this convert.", "warning")
642+
return redirect(url_for("account.login"))
643+
644+
if current_user.id != convert_obj.user_id and not current_user.is_admin():
645+
flash("You do not have permission to view this convert.", "danger")
646+
return redirect(url_for("convert.history"))
647+
648+
return render_template(
649+
"convert/compare_version/difference.html",
650+
old_result=convert_obj_history.old_output_text,
651+
new_result=convert_obj_history.new_output_text,
652+
convert_obj=convert_obj,
653+
history_id=convert_obj_history.id
654+
)
655+
else:
656+
return render_template(
657+
"convert/compare_version/difference.html",
658+
old_result=convert_obj_history.old_output_text,
659+
new_result=convert_obj_history.new_output_text,
660+
convert_obj=convert_obj,
661+
history_id=convert_obj_history.id
662+
)
663+
664+
# get_history_details
665+
@convert_blueprint.route("/get_history_details", methods=['GET'])
666+
def get_history_details():
667+
"""Get the details of a convert history entry"""
668+
history_id = request.args.get('history_id', type=int)
669+
if history_id:
670+
convert_history = ConvertModel.get_convert_history_by_id(history_id)
671+
if convert_history:
672+
return {
673+
"success": True,
674+
"history": convert_history.to_json(),
675+
"message": "Convert history found",
676+
"toast_class" : "success"
677+
}, 200
678+
return {
679+
"success": False,
680+
"message": "No convert history found for this id",
681+
"toast_class" : "danger"
682+
}, 500
683+
return {
684+
"success": False,
685+
"message": "No history_id provided",
686+
"toast_class" : "danger"
687+
}, 500

0 commit comments

Comments
 (0)